using AxCopilot.Models; namespace AxCopilot.Services.Agent; public partial class AgentLoopService { private void ApplyCodeQualityFollowUpTransition( ContentBlock call, ToolResult result, List messages, TaskTypePolicy taskPolicy, ref bool requireHighImpactCodeVerification, ref string? lastModifiedCodeFilePath) { var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result); requireHighImpactCodeVerification = highImpactCodeChange; if (highImpactCodeChange && ShouldInjectCodeQualityFollowUp(ActiveTab ?? "", call.ToolName, result)) { lastModifiedCodeFilePath = result.FilePath; messages.Add(new ChatMessage { Role = "user", Content = BuildCodeQualityFollowUpPrompt( call.ToolName, result, highImpactCodeChange, HasAnyBuildOrTestEvidence(messages), taskPolicy) }); EmitEvent(AgentEventType.Thinking, "", highImpactCodeChange ? "고영향 코드 변경으로 분류되어 참조 검증과 build/test 검증을 더 엄격하게 이어갑니다." : "코드 변경 이후 build/test/diff 검증을 이어갑니다."); } else if (HasCodeVerificationEvidenceAfterLastModification(messages, requireHighImpactCodeVerification)) { requireHighImpactCodeVerification = false; } } private bool TryApplyCodeCompletionGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, bool requireHighImpactCodeVerification, int totalToolCalls, AgentContext context, ExplorationTrackingState explorationState, bool workspaceWasInitiallyEmpty, RunState runState, ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase) || totalToolCalls <= 0) return false; if (TryApplyProjectLayoutGateTransition( messages, textResponse, context, explorationState, workspaceWasInitiallyEmpty, runState)) return true; var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification( messages, requireHighImpactCodeVerification); var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages); var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages); var hasSuccessfulBuildAndTestEvidence = HasSuccessfulBuildAndTestAfterLastModification(messages); var hasLightweightCompletionEvidence = requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasDiffEvidence || hasRecentBuildOrTestEvidence || hasCodeVerificationEvidence; if (executionPolicy.CodeVerificationGateMaxRetries > 0 && !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence) && runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries) { runState.CodeVerificationGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = requireHighImpactCodeVerification ? "[System:CodeQualityGate] 공용/고영향 코드 변경 이후 검증 근거가 부족합니다. 종료하지 말고 file_read, grep/glob, git diff, build/test까지 확인한 뒤에만 마무리하세요." : "[System:CodeQualityGate] 마지막 코드 수정 이후 build/test/file_read/diff 근거가 부족합니다. 종료하지 말고 검증 근거를 보강한 뒤에만 마무리하세요." }); EmitEvent(AgentEventType.Thinking, "", requireHighImpactCodeVerification ? "고영향 코드 변경의 검증 근거가 부족해 추가 검증을 진행합니다..." : "코드 결과 검증 근거가 부족해 추가 검증을 진행합니다..."); return true; } if (requireHighImpactCodeVerification && executionPolicy.HighImpactBuildTestGateMaxRetries > 0 && !HasSuccessfulBuildAndTestAfterLastModification(messages) && runState.HighImpactBuildTestGateRetry < executionPolicy.HighImpactBuildTestGateMaxRetries) { runState.HighImpactBuildTestGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = "[System:HighImpactBuildTestGate] 고영향 코드 변경입니다. 종료하지 말고 build_run과 test_loop를 모두 실행해 성공 근거를 확보한 뒤에만 마무리하세요." }); EmitEvent(AgentEventType.Thinking, "", "고영향 변경이라 build와 test 성공 근거를 모두 확보할 때까지 진행합니다..."); return true; } var hasBlockingCodeEvidenceGap = !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence) || (requireHighImpactCodeVerification && !hasSuccessfulBuildAndTestEvidence); var shouldRequestStructuredFinalReport = taskPolicy.IsReviewTask || requireHighImpactCodeVerification || taskPolicy.TaskType is "bugfix" or "feature" or "refactor" or "docs"; if (executionPolicy.FinalReportGateMaxRetries > 0 && shouldRequestStructuredFinalReport && !hasBlockingCodeEvidenceGap && !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages) && runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries) { runState.FinalReportGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = BuildFinalReportQualityPrompt(taskPolicy, requireHighImpactCodeVerification) }); EmitEvent(AgentEventType.Thinking, "", "최종 보고에 변경·검증·리스크 요약이 부족해 한 번 더 정리합니다..."); return true; } return false; } private bool TryApplyProjectLayoutGateTransition( List messages, string? textResponse, AgentContext context, ExplorationTrackingState explorationState, bool workspaceWasInitiallyEmpty, RunState runState) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; var scaffoldProfile = explorationState.ScaffoldProfile; if (!workspaceWasInitiallyEmpty || scaffoldProfile == null) return false; if (runState.ProjectLayoutGateRetry >= 1) return false; if (!HasProjectScaffoldModificationEvidence(messages)) return false; var assessment = ProjectScaffoldProfileCatalog.AssessLayout(scaffoldProfile, context.WorkFolder); if (assessment.IsSatisfied) return false; runState.ProjectLayoutGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = BuildProjectLayoutGatePrompt(scaffoldProfile, assessment) }); EmitEvent( AgentEventType.Thinking, "", $"프로젝트 구조가 평면적으로 생성되어 폴더 레이아웃을 먼저 정리합니다. ({runState.ProjectLayoutGateRetry}/1)"); return true; } private static bool HasProjectScaffoldModificationEvidence(List messages) { foreach (var message in messages) { if (!TryGetToolResultToolName(message, out var toolName)) continue; if (toolName is "file_write" or "file_edit" or "file_manage" or "script_create") return true; } return false; } private static string BuildProjectLayoutGatePrompt( ProjectScaffoldProfile profile, ProjectScaffoldLayoutAssessment assessment) { var missingDirectories = assessment.MissingDirectories.Count == 0 ? "none" : string.Join(", ", assessment.MissingDirectories.Take(6)); var suspiciousRootFiles = assessment.SuspiciousRootFiles.Count == 0 ? "none" : string.Join(", ", assessment.SuspiciousRootFiles.Take(8)); var allowedRootFiles = profile.AllowedRootFiles.Count == 0 ? "only manifest and entry files" : string.Join(", ", profile.AllowedRootFiles); return "[System:ProjectLayoutGate] This looks like a structured scaffold request for " + $"{profile.Label}, but the current workspace layout is still too flat. " + $"Create or complete folders such as {ProjectScaffoldProfileCatalog.BuildDirectoryPreview(profile)}, " + $"especially the missing ones: {missingDirectories}. " + $"Move implementation files that are still sitting in the workspace root into the proper folders: {suspiciousRootFiles}. " + $"Keep only appropriate root files such as {allowedRootFiles}. " + "Use file_manage(mkdir/move) plus file_edit/file_write to reorganize the scaffold before finishing, " + "then rerun build/test if relevant and only after that summarize the result."; } private bool TryApplyCodeDiffEvidenceGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, RunState runState, ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; if (!taskPolicy.IsReviewTask) return false; if (executionPolicy.CodeDiffGateMaxRetries <= 0 || runState.CodeDiffGateRetry >= executionPolicy.CodeDiffGateMaxRetries) return false; if (HasDiffEvidenceAfterLastModification(messages) || HasBuildOrTestEvidenceAfterLastModification(messages)) return false; runState.CodeDiffGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = "[System:CodeDiffGate] 코드 변경 이후 diff 근거가 부족합니다. git_tool로 변경 파일과 핵심 diff를 먼저 확인하고 요약하세요. 지금 즉시 git_tool을 호출하세요." }); EmitEvent(AgentEventType.Thinking, "", "코드 diff 근거가 부족해 git diff 검증을 추가합니다..."); return true; } private bool TryApplyRecentExecutionEvidenceGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, RunState runState, ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; if (!ShouldApplyExecutionResultGate(taskPolicy)) return false; if (executionPolicy.RecentExecutionGateMaxRetries <= 0 || runState.RecentExecutionGateRetry >= executionPolicy.RecentExecutionGateMaxRetries) return false; if (HasDiffEvidenceAfterLastModification(messages)) return false; if (!HasAnyBuildOrTestEvidence(messages)) return false; if (HasBuildOrTestEvidenceAfterLastModification(messages)) return false; runState.RecentExecutionGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = BuildRecentExecutionEvidencePrompt(taskPolicy) }); EmitEvent(AgentEventType.Thinking, "", "최근 수정 이후 실행 근거가 부족해 build/test 재검증을 진행합니다..."); return true; } private bool TryApplyExecutionSuccessGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, RunState runState, ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; if (!ShouldApplyExecutionResultGate(taskPolicy)) return false; if (executionPolicy.ExecutionSuccessGateMaxRetries <= 0 || runState.ExecutionSuccessGateRetry >= executionPolicy.ExecutionSuccessGateMaxRetries) return false; if (HasDiffEvidenceAfterLastModification(messages)) return false; if (!HasAnyBuildOrTestAttempt(messages)) return false; if (HasAnyBuildOrTestEvidence(messages)) return false; runState.ExecutionSuccessGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = BuildExecutionSuccessGatePrompt(taskPolicy) }); EmitEvent(AgentEventType.Thinking, "", "실패한 실행 근거만 있어 build/test 성공 결과를 다시 검증합니다..."); 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, TaskTypePolicy taskPolicy, string userQuery, int totalToolCalls, string? lastArtifactFilePath, RunState runState, int retryMax) { if (totalToolCalls <= 0) return false; if (ShouldSkipTerminalEvidenceGateForAnalysisQuery(userQuery, taskPolicy)) return false; if (runState.TerminalEvidenceGateRetry >= retryMax) return false; if (HasTerminalProgressEvidence(taskPolicy, messages, lastArtifactFilePath)) return false; runState.TerminalEvidenceGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = BuildTerminalEvidenceGatePrompt(taskPolicy, lastArtifactFilePath) }); EmitEvent(AgentEventType.Thinking, "", $"종료 전 실행 근거가 부족해 보강 단계를 진행합니다. ({runState.TerminalEvidenceGateRetry}/{retryMax})"); return true; } }