diff --git a/README.md b/README.md index 334e33d..65e2c2c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index cf4e93e..f878338 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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 diff --git a/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs b/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs index 3a33406..371dceb 100644 --- a/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs +++ b/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs @@ -206,6 +206,25 @@ public class AgentLoopCodeQualityTests refactorPrompt.Should().Contain("behavior-compatible"); } + [Fact] + public void BuildFailureInvestigationPrompt_PrefersMultiReadWhenBuildOutputMentionsMultipleFiles() + { + var prompt = InvokePrivateStatic( + "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( + "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() { diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index 80ae5b8..ceb3af9 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -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 ExtractFailurePathHintsFromToolOutput(string? toolOutput, int maxHints = 4) + { + var hints = new List(); + if (string.IsNullOrWhiteSpace(toolOutput)) + return hints; + + var allowedExt = new HashSet(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); diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs index f018abf..8057db7 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs @@ -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 messages, string? textResponse, diff --git a/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs b/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs index bc17cb1..b53b4e7 100644 --- a/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs +++ b/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs @@ -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), };