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

- 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

@@ -2317,3 +2317,12 @@ MIT License
- 寃€利?
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_background_conversation_live_ui\\ -p:IntermediateOutputPath=obj\\verify_background_conversation_live_ui\\` 寃쎄퀬 0 / ?ㅻ쪟 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|AppStateServiceTests" -p:OutputPath=bin\\verify_background_conversation_live_ui_tests\\ -p:IntermediateOutputPath=obj\\verify_background_conversation_live_ui_tests\\` ?듦낵 49
업데이트: 2026-04-15 23:03 (KST)
- Code 탭 빌드 실패 후 조기 종료 회귀를 보강했습니다. `src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs`가 이제 `review`뿐 아니라 `bugfix/feature/refactor`에도 최근 실행 근거 게이트와 실행 성공 게이트를 적용해, 실패한 `build_run/test_loop` 뒤에 모델이 읽기만 하고 텍스트 종료로 빠지는 흐름을 한 번 더 실행 복구 경로로 밀어줍니다.
- `src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs``balanced`, `reasoning_first`, `tool_call_strict` 프로파일에 `RecentExecutionGateMaxRetries=1`, `ExecutionSuccessGateMaxRetries=1`을 부여했습니다. 코드 작업 중심 프로파일에서는 최소 1회 이상 `실패 후 수정/재검증` 루프를 허용하도록 맞춘 것입니다.
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`의 실패 조사 프롬프트는 빌드 로그에 오류 파일이 2개 이상 잡히면 `multi_read`로 함께 읽도록 유도합니다. 예를 들어 `Themes\\ControlStyles.xaml`, `Themes\\Effects.xaml`처럼 동시 오류가 난 경우 단건 `file_read` 반복보다 빠르게 원인을 좁히는 쪽으로 유도합니다.
- 테스트:
- `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`에 다중 오류 파일 `multi_read` 유도, 코드 작업용 실행 게이트 적용 범위, 코드 중심 실행 프로파일 게이트 활성화 회귀 테스트를 추가했습니다.
- 검증:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_build_failure_recovery\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery\\` 경고 0 / 오류 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_build_failure_recovery_tests\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery_tests\\` 통과 134

View File

@@ -1642,3 +1642,18 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
- 검증:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_html_legacy_body_style\\ -p:IntermediateOutputPath=obj\\verify_html_legacy_body_style\\` 경고 0 / 오류 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "HtmlSkillLegacyBodyCompatibilityTests|HtmlSkillConsultingSectionsTests" -p:OutputPath=bin\\verify_html_legacy_body_style_tests_relevant\\ -p:IntermediateOutputPath=obj\\verify_html_legacy_body_style_tests_relevant\\` 통과 2
업데이트: 2026-04-15 23:03 (KST)
- Code 탭의 빌드 실패 복구 경로를 강화했습니다. 최근 `build_run` 실패 뒤 `file_read` 1회만 수행하고 텍스트 종료로 빠진 로그를 기준으로 `src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs`를 조정해 `review` 전용이던 최근 실행 근거/실행 성공 게이트를 `bugfix`, `feature`, `refactor`에도 적용합니다. 이제 코드 생성/수정 작업에서 실패한 `build_run/test_loop` 후 텍스트 종료가 나오면 최소 1회는 다시 수정·재검증 경로를 요구합니다.
- 실행 프로파일도 함께 보강했습니다. `src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs``balanced`, `reasoning_first`, `tool_call_strict` 프로파일에 `RecentExecutionGateMaxRetries=1`, `ExecutionSuccessGateMaxRetries=1`을 설정해, 코드 작업 프로파일에서 빌드 실패 이후 그대로 끝나는 것을 기본 정책 차원에서 막았습니다. `fast_readonly`, `document_heavy`는 문서/읽기 위주 성격을 유지하기 위해 그대로 두었습니다.
- 빌드 실패 조사 프롬프트는 다중 오류 파일을 더 빠르게 좁히도록 바꿨습니다. `src/AxCopilot/Services/Agent/AgentLoopService.cs``BuildFailureInvestigationPrompt(...)`가 빌드 출력에서 `ControlStyles.xaml`, `Effects.xaml` 같은 오류 파일 힌트를 2개 이상 추출하면 `multi_read`를 우선 사용하라고 지시합니다. 단일 오류 파일일 때만 `file_read`를 유지하고, 로그에서 파일을 못 뽑을 때는 기존 최근 수정 파일 fallback을 사용합니다.
- 이번 분석에서 확인한 최근 WPF 지뢰찾기 생성 실행의 직접 원인은 두 가지였습니다.
- 실제 빌드 오류: `Themes\\ControlStyles.xaml``SelectionTextColor` 속성과 `Themes\\Effects.xaml``DropShadowEffect Opacity` 속성이 WPF XAML에서 유효하지 않아 `MC4005`, `MC3072`가 발생했습니다.
- 조기 종료 원인: 당시 기본 실행 프로파일에서 최근 실행/성공 게이트가 0회였고, 코드 작업은 `review`가 아니라서 게이트 적용도 건너뛰고 있었습니다. 그 결과 실패 직후 `file_read` 1회 이후 텍스트 종료가 그대로 수용됐습니다.
- 현재 병렬 처리 상태도 함께 점검했습니다.
- `src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs`는 동일 LLM 응답 안에서 나온 읽기 전용 도구 묶음만 병렬 실행합니다. 내부 최대 동시성은 기본 4개이며 `AXCOPILOT_MAX_PARALLEL_TOOLS`로 상한을 조정할 수 있습니다.
- `src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs`의 읽기 병렬 배치(`EnableParallelReadBatch`, `MaxParallelReadBatch`)는 켜져 있지만, `src/AxCopilot/Services/LlmService.ToolUse.cs`의 OpenAI 호환 프로필은 `vllm + deepseek/qwen/llama/mistral` 계열에서 `parallel_tool_calls=false`로 보수적으로 동작합니다. 즉 현재는 “모델이 한 번에 여러 읽기 호출을 내면 병렬 실행”은 가능하지만, 쓰기/빌드/테스트 단계까지 폭넓은 병렬화는 아직 아닙니다.
- 테스트:
- `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`에 다중 오류 파일 `multi_read` 유도, 코드 작업용 실행 게이트 적용 범위, 코드 중심 실행 프로파일 게이트 활성화 테스트를 추가했습니다.
- 검증:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_build_failure_recovery\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery\\` 경고 0 / 오류 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_build_failure_recovery_tests\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery_tests\\` 통과 134

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),
};