- 앱 생성 진행/도구/완료 카드에 전용 최대폭을 도입하고 좌측 정렬로 통일 - 라이브 진행 카드와 검증 게이트 문구에서 깨져 보이던 문자열을 정상화 - build_run/process 도구가 Windows 기본 출력 인코딩을 우선 사용하도록 조정 - README와 DEVELOPMENT 문서에 2026-04-16 00:57 (KST) 기준 이력 반영 검증: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_agent_ui_layout_encoding\ -p:IntermediateOutputPath=obj\verify_agent_ui_layout_encoding\ (경고 0 / 오류 0) - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatWindowSlashPolicyTests|AgentLoopCodeQualityTests" -p:OutputPath=bin\verify_agent_ui_layout_encoding_tests\ -p:IntermediateOutputPath=obj\verify_agent_ui_layout_encoding_tests\ (통과 194)
361 lines
16 KiB
C#
361 lines
16 KiB
C#
using AxCopilot.Models;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
public partial class AgentLoopService
|
|
{
|
|
private void ApplyCodeQualityFollowUpTransition(
|
|
ContentBlock call,
|
|
ToolResult result,
|
|
List<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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;
|
|
}
|
|
}
|