using AxCopilot.Models; using AxCopilot.Services; using System.Text.Json; namespace AxCopilot.Services.Agent; public partial class AgentLoopService { private bool TryHandleContextOverflowTransition( Exception ex, List messages, RunState runState) { if (!IsContextOverflowError(ex.Message) || runState.ContextRecoveryAttempts >= 2) return false; runState.ContextRecoveryAttempts++; var recovered = ForceContextRecovery(messages); EmitEvent( AgentEventType.Thinking, "", recovered ? $"컨텍스트 초과를 감지해 자동 복구 후 재시도합니다 ({runState.ContextRecoveryAttempts}/2)" : $"컨텍스트 초과를 감지했지만 복구할 충분한 이력이 없어 재시도하지 못했습니다 ({runState.ContextRecoveryAttempts}/2)"); return recovered; } private bool TryHandleWithheldResponseTransition( string? textResponse, List messages, RunState runState) { if (string.IsNullOrWhiteSpace(textResponse)) return false; if (!IsLikelyWithheldOrOverflowResponse(textResponse)) return false; if (runState.WithheldRecoveryAttempts >= 2) return false; runState.WithheldRecoveryAttempts++; var recovered = ForceContextRecovery(messages); messages.Add(new ChatMessage { Role = "user", Content = "[System:WithheldRecovery] 직전 응답이 길이/토큰 한계로 중단된 것으로 보입니다. " + "컨텍스트를 정리했으니 직전 작업을 이어서 진행하세요. " + "필요하면 먼저 핵심만 요약하고 다음 도구 호출로 진행하세요." }); EmitEvent( AgentEventType.Thinking, "", recovered ? $"withheld/길이 제한 응답을 감지해 컨텍스트를 복구하고 재시도합니다 ({runState.WithheldRecoveryAttempts}/2)" : $"withheld/길이 제한 응답을 감지해 최소 지시로 재시도합니다 ({runState.WithheldRecoveryAttempts}/2)"); return true; } private bool TryApplyCodeCompletionGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, bool requireHighImpactCodeVerification, int totalToolCalls, RunState runState) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase) || totalToolCalls <= 0) return false; var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification( messages, requireHighImpactCodeVerification); if (!hasCodeVerificationEvidence && runState.CodeVerificationGateRetry < 2) { 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 && !HasSuccessfulBuildAndTestAfterLastModification(messages) && runState.HighImpactBuildTestGateRetry < 1) { 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; } if (!HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages) && runState.FinalReportGateRetry < 1) { 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 TryApplyCodeDiffEvidenceGateTransition( List messages, string? textResponse, RunState runState) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; if (runState.CodeDiffGateRetry >= 1) return false; if (HasDiffEvidenceAfterLastModification(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) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; if (runState.RecentExecutionGateRetry >= 1) 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) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; if (runState.ExecutionSuccessGateRetry >= 1) 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 bool TryApplyTerminalEvidenceGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, string userQuery, int totalToolCalls, string? lastArtifactFilePath, RunState runState) { if (totalToolCalls <= 0) return false; if (ShouldSkipTerminalEvidenceGateForAnalysisQuery(userQuery, taskPolicy)) return false; var retryMax = GetTerminalEvidenceGateMaxRetries(); 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; } private static bool ShouldSkipTerminalEvidenceGateForAnalysisQuery(string userQuery, TaskTypePolicy taskPolicy) { if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) return false; if (string.IsNullOrWhiteSpace(userQuery)) return false; return ContainsAny( userQuery, "설명", "분석", "요약", "리뷰", "explain", "analyze", "summary", "review"); } private static bool HasTerminalProgressEvidence( TaskTypePolicy taskPolicy, List messages, string? lastArtifactFilePath) { if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) { return HasMaterializedArtifact(lastArtifactFilePath) && HasDocumentVerificationEvidenceAfterLastArtifact(messages); } return HasAnySuccessfulProgressToolResult(messages) || HasMaterializedArtifact(lastArtifactFilePath); } private static bool HasAnySuccessfulProgressToolResult(List messages) { foreach (var message in messages.AsEnumerable().Reverse()) { if (!TryGetToolResultInfo(message, out var toolName, out var content)) continue; if (!IsMutatingOrExecutionProgressTool(toolName)) continue; if (IsLikelyFailureContent(content)) continue; return true; } return false; } private static string BuildTerminalEvidenceGatePrompt(TaskTypePolicy taskPolicy, string? lastArtifactFilePath) { if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) { var fileHint = string.IsNullOrWhiteSpace(lastArtifactFilePath) ? "寃곌낵 臾몄꽌 ?뚯씪" : $"'{lastArtifactFilePath}'"; return "[System:TerminalEvidenceGate] ?꾩옱 醫낅즺 ?묐떟?먮뒗 ?ㅽ뻾 寃곌낵 利앷굅媛€ 遺€議깊빀?덈떎. " + $"{fileHint}???ㅼ젣濡??앹꽦/媛깆떊?섍퀬 file_read ?먮뒗 document_read濡?寃€利앺븳 洹쇨굅瑜??④릿 ??醫낅즺?섏꽭??"; } return "[System:TerminalEvidenceGate] ?꾩옱 醫낅즺 ?묐떟?먮뒗 ?ㅽ뻾 寃곌낵 利앷굅媛€ 遺€議깊빀?덈떎. " + "理쒖냼 1媛??댁긽??吏꾪뻾 ?꾧뎄(?섏젙/?ㅽ뻾/?앹꽦)瑜??깃났?쒗궎怨? 洹?寃곌낵瑜?洹쇨굅濡?理쒖쥌 ?묐떟???ㅼ떆 ?묒꽦?섏꽭??"; } private static string BuildRecentExecutionEvidencePrompt(TaskTypePolicy taskPolicy) { var taskHint = taskPolicy.TaskType switch { "bugfix" => "?ы쁽 寃쎈줈 湲곗??쇰줈 ?섏젙 吏€?먯씠 ?ㅼ젣濡??닿껐?먮뒗吏€ 寃€利앺븯?몄슂.", "feature" => "?좉퇋 ?숈옉 寃쎈줈???뺤긽/?ㅻ쪟 耳€?댁뒪瑜?理쒖냼 1媛??댁긽 ?뺤씤?섏꽭??", "refactor" => "湲곗〈 ?숈옉 蹂댁〈 ?щ?瑜??뚭? 愿€?먯쑝濡??뺤씤?섏꽭??", _ => "?섏젙 ?곹뼢 踰붿쐞瑜?湲곗??쇰줈 ?ㅽ뻾 寃€利앹쓣 吏꾪뻾?섏꽭??" }; return "[System:RecentExecutionGate] 留덉?留?肄붾뱶 ?섏젙 ?댄썑 build/test ?ㅽ뻾 洹쇨굅媛€ ?놁뒿?덈떎. " + "吏€湲?利됱떆 build_run ?먮뒗 test_loop瑜??몄텧??理쒖떊 ?곹깭瑜?寃€利앺븯怨?寃곌낵瑜??④린?몄슂. " + taskHint; } private static string BuildExecutionSuccessGatePrompt(TaskTypePolicy taskPolicy) { var taskHint = taskPolicy.TaskType switch { "bugfix" => "?ㅽ뙣 ?먯씤??癒쇱? ?섏젙?????숈씪 ?ы쁽 寃쎈줈 湲곗??쇰줈 ?뚯뒪?몃? ?ㅼ떆 ?듦낵?쒗궎?몄슂.", "feature" => "?듭떖 ?좉퇋 ?숈옉 寃쎈줈瑜??ы븿??build/test瑜??듦낵?쒗궎?몄슂.", "refactor" => "?뚭? ?щ?瑜??뺤씤?????덈뒗 ?뚯뒪?몃? ?듦낵?쒗궎?몄슂.", _ => "?섏젙 ?곹뼢 踰붿쐞瑜??뺤씤?????덈뒗 ?ㅽ뻾 寃€利앹쓣 ?듦낵?쒗궎?몄슂." }; return "[System:ExecutionSuccessGate] ?꾩옱 ?ㅽ뻾 洹쇨굅媛€ ?ㅽ뙣 寃곌낵肉먯엯?덈떎. " + "醫낅즺?섏? 留먭퀬 build_run ?먮뒗 test_loop瑜??몄텧???깃났 寃곌낵瑜??뺣낫?섏꽭?? " + taskHint; } private static bool HasDiffEvidenceAfterLastModification(List messages) { var modificationTools = new HashSet(StringComparer.OrdinalIgnoreCase) { "file_edit", "file_write", "file_manage", "html_create", "markdown_create", "docx_create", "excel_create", "csv_create", "pptx_create", "chart_create", "document_assemble", "document_plan" }; var sawDiff = false; foreach (var message in messages.AsEnumerable().Reverse()) { if (!TryGetToolResultInfo(message, out var toolName, out var content)) continue; if (string.Equals(toolName, "git_tool", StringComparison.OrdinalIgnoreCase)) { sawDiff = IsLikelyDiffEvidenceContent(content); continue; } if (modificationTools.Contains(toolName)) return sawDiff; } // 蹂€寃??꾧뎄 ?먯껜媛€ ?놁쑝硫?diff 寃뚯씠?몃? ?붽뎄?섏? ?딆쓬 return true; } private static bool IsLikelyDiffEvidenceContent(string? content) { if (string.IsNullOrWhiteSpace(content)) return false; return ContainsAny( content, "diff --git", "@@ ", "+++ ", "--- ", "changed files", "insertions", "deletions", "file changed", "異붽?", "??젣"); } private bool TryApplyDocumentArtifactGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, string? lastArtifactFilePath, string? suggestedPath, RunState runState) { if (!ShouldRequestDocumentArtifact(taskPolicy.TaskType, lastArtifactFilePath, runState.DocumentArtifactGateRetry)) return false; runState.DocumentArtifactGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); var targetHint = string.IsNullOrWhiteSpace(suggestedPath) ? "?붿껌???곗텧臾?臾몄꽌 ?뚯씪" : $"'{suggestedPath}'"; messages.Add(new ChatMessage { Role = "user", Content = "[System:DocumentArtifactGate] 寃곌낵 ?ㅻ챸留뚯쑝濡?醫낅즺?????놁뒿?덈떎. " + $"吏€湲?利됱떆 {targetHint}???ㅼ젣 ?뚯씪濡??앹꽦?섏꽭?? " + "html_create/markdown_create/document_assemble/file_write 以??곸젅???꾧뎄瑜?諛섎뱶???몄텧?섏꽭??" }); EmitEvent( AgentEventType.Thinking, "", $"臾몄꽌 ?묒뾽 ?곗텧臾??뚯씪???놁뼱 ?꾧뎄 ?ㅽ뻾???ъ슂泥?빀?덈떎 ({runState.DocumentArtifactGateRetry}/2)"); return true; } private bool TryApplyDocumentVerificationGateTransition( List messages, string? textResponse, TaskTypePolicy taskPolicy, string? lastArtifactFilePath, RunState runState) { if (!ShouldRequestDocumentVerification(taskPolicy.TaskType, lastArtifactFilePath, messages, runState.DocumentVerificationGateRetry)) return false; runState.DocumentVerificationGateRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); messages.Add(new ChatMessage { Role = "user", Content = "[System:DocumentVerificationGate] 臾몄꽌 ?뚯씪?€ ?앹꽦?섏뿀吏€留?寃€利?洹쇨굅媛€ 遺€議깊빀?덈떎. " + "file_read ?먮뒗 document_read濡?諛⑷툑 ?앹꽦???뚯씪???ㅼ떆 ?쎄퀬, 臾몄젣 ?놁쓬/?섏젙 ?꾩슂瑜?紐낆떆??蹂닿퀬?섏꽭??" }); EmitEvent( AgentEventType.Thinking, "", $"臾몄꽌 寃€利?洹쇨굅媛€ 遺€議깊빐 ?ш?利앹쓣 ?붿껌?⑸땲??({runState.DocumentVerificationGateRetry}/1)"); return true; } private static bool ShouldRequestDocumentArtifact( string taskType, string? lastArtifactFilePath, int artifactGateRetry) { return string.Equals(taskType, "docs", StringComparison.OrdinalIgnoreCase) && !HasMaterializedArtifact(lastArtifactFilePath) && artifactGateRetry < 2; } private static bool ShouldRequestDocumentVerification( string taskType, string? lastArtifactFilePath, List messages, int verificationGateRetry) { return string.Equals(taskType, "docs", StringComparison.OrdinalIgnoreCase) && HasMaterializedArtifact(lastArtifactFilePath) && !HasDocumentVerificationEvidenceAfterLastArtifact(messages) && verificationGateRetry < 1; } private static bool HasDocumentVerificationEvidenceAfterLastArtifact(List messages) { var artifactTools = new HashSet(StringComparer.OrdinalIgnoreCase) { "html_create", "markdown_create", "docx_create", "excel_create", "csv_create", "pptx_create", "chart_create", "document_assemble", "file_write" }; var verificationTools = new HashSet(StringComparer.OrdinalIgnoreCase) { "file_read", "document_read", "document_review" }; var sawVerification = false; foreach (var message in messages.AsEnumerable().Reverse()) { if (!TryGetToolResultToolName(message, out var toolName)) continue; if (verificationTools.Contains(toolName)) { sawVerification = true; continue; } if (artifactTools.Contains(toolName)) return sawVerification; } return sawVerification; } private static bool HasMaterializedArtifact(string? path) { if (string.IsNullOrWhiteSpace(path)) return false; try { return System.IO.File.Exists(path); } catch { return false; } } private static bool IsContextOverflowError(string? message) { if (string.IsNullOrWhiteSpace(message)) return false; var lower = message.ToLowerInvariant(); return lower.Contains("prompt too long") || lower.Contains("prompt is too long") || lower.Contains("context length") || lower.Contains("context window") || lower.Contains("too many tokens") || lower.Contains("max tokens") || lower.Contains("max_output_tokens") || lower.Contains("maximum output tokens") || lower.Contains("token limit") || lower.Contains("input is too long") || lower.Contains("response was truncated") || lower.Contains("maximum context"); } private static bool IsLikelyWithheldOrOverflowResponse(string response) { var lower = response.ToLowerInvariant(); return lower.Contains("withheld") || lower.Contains("prompt too long") || lower.Contains("context window") || lower.Contains("max_output_tokens") || lower.Contains("maximum output tokens") || lower.Contains("token limit") || lower.Contains("response was truncated") || lower.Contains("input is too long"); } private static bool ForceContextRecovery(List messages) { if (messages.Count < 10) return false; var system = messages.FirstOrDefault(m => string.Equals(m.Role, "system", StringComparison.OrdinalIgnoreCase)); var nonSystem = messages.Where(m => !string.Equals(m.Role, "system", StringComparison.OrdinalIgnoreCase)).ToList(); if (nonSystem.Count < 8) return false; const int keepTailCount = 8; var removed = nonSystem.Take(Math.Max(0, nonSystem.Count - keepTailCount)).ToList(); if (removed.Count < 3) return false; var summaryLines = removed .Where(m => !string.IsNullOrWhiteSpace(m.Content)) .Take(14) .Select(m => { var content = m.Content ?? ""; if (content.StartsWith("{\"_tool_use_blocks\"")) content = "[?꾧뎄 ?몄텧 ?붿빟]"; else if (content.StartsWith("{\"type\":\"tool_result\"")) content = "[?꾧뎄 ?ㅽ뻾 寃곌낵 ?붿빟]"; else if (content.Length > 180) content = content[..180] + "..."; return $"- [{m.Role}] {content}"; }) .ToList(); var summary = summaryLines.Count > 0 ? string.Join("\n", summaryLines) : "- ?댁쟾 ?€??留λ씫???먮룞 異뺤빟?섏뿀?듬땲??"; var tail = nonSystem.Skip(Math.Max(0, nonSystem.Count - keepTailCount)).ToList(); messages.Clear(); if (system != null) messages.Add(system); messages.Add(new ChatMessage { Role = "user", Timestamp = DateTime.Now, Content = $"[?쒖뒪???먮룞 留λ씫 異뺤빟]\n?꾨옒???댁쟾 ?€?붿쓽 ?듭떖 ?붿빟?낅땲??\n{summary}" }); messages.AddRange(tail); return true; } private bool TryHandleRepeatedFailureGuardTransition( LlmService.ContentBlock call, string toolCallSignature, List messages, string? lastFailedToolSignature, int repeatedFailedToolSignatureCount, int maxRetry, string? lastModifiedCodeFilePath, bool requireHighImpactCodeVerification, TaskTypePolicy taskPolicy) { if (!ShouldBlockRepeatedFailedCall( toolCallSignature, lastFailedToolSignature, repeatedFailedToolSignatureCount, maxRetry)) return false; var repeatedGuardMsg = BuildRepeatedFailureGuardMessage( call.ToolName, repeatedFailedToolSignatureCount, maxRetry); messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, repeatedGuardMsg)); messages.Add(new ChatMessage { Role = "user", Content = BuildRepeatedFailureRecoveryPrompt( call.ToolName, lastModifiedCodeFilePath, requireHighImpactCodeVerification, taskPolicy) }); EmitEvent( AgentEventType.Thinking, call.ToolName, $"?숈씪 ?꾧뎄/?뚮씪誘명꽣 諛섎났 ?ㅽ뙣 ?⑦꽩 媛먯? - ?ㅻⅨ ?묎렐?쇰줈 ?꾪솚?⑸땲??({repeatedFailedToolSignatureCount}/{maxRetry})"); return true; } private bool TryHandleNoProgressReadOnlyLoopTransition( LlmService.ContentBlock call, string toolCallSignature, int repeatedSameSignatureCount, List messages, string? lastModifiedCodeFilePath, bool requireHighImpactCodeVerification, TaskTypePolicy taskPolicy) { if (!ShouldBlockNoProgressReadOnlyLoop( call.ToolName, repeatedSameSignatureCount)) return false; messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, $"[NO_PROGRESS_LOOP_GUARD] ?숈씪???쎄린 ?꾧뎄 ?몄텧??{repeatedSameSignatureCount}??諛섎났?섏뿀?듬땲?? {toolCallSignature}\n" + "Stop repeating the same read-only call and switch to a concrete next action.")); messages.Add(new ChatMessage { Role = "user", Content = BuildNoProgressLoopRecoveryPrompt( call.ToolName, lastModifiedCodeFilePath, requireHighImpactCodeVerification, taskPolicy) }); EmitEvent( AgentEventType.Thinking, call.ToolName, $"臾댁쓽誘명븳 ?쎄린 ?꾧뎄 諛섎났 猷⑦봽瑜?媛먯????ㅻⅨ ?꾨왂?쇰줈 ?꾪솚?⑸땲??({repeatedSameSignatureCount}??"); return true; } private bool TryHandleReadOnlyStagnationTransition( int consecutiveReadOnlySuccessTools, List messages, string? lastModifiedCodeFilePath, bool requireHighImpactCodeVerification, TaskTypePolicy taskPolicy) { var threshold = GetReadOnlyStagnationThreshold(); if (consecutiveReadOnlySuccessTools < threshold) return false; messages.Add(new ChatMessage { Role = "user", Content = BuildNoProgressLoopRecoveryPrompt( "read_only_loop", lastModifiedCodeFilePath, requireHighImpactCodeVerification, taskPolicy) }); EmitEvent( AgentEventType.Thinking, "", $"?쎄린 ?꾩슜 ?꾧뎄留??곗냽 {consecutiveReadOnlySuccessTools}???ㅽ뻾?섏뼱 ?뺤껜瑜?媛먯??덉뒿?덈떎. ?ㅽ뻾 ?④퀎濡??꾪솚?⑸땲?? (threshold={threshold})"); return true; } private bool TryHandleNoProgressExecutionTransition( int consecutiveNonMutatingSuccessTools, List messages, string? lastModifiedCodeFilePath, bool requireHighImpactCodeVerification, TaskTypePolicy taskPolicy, string? documentPlanPath, RunState runState) { if (!ShouldTriggerNoProgressExecutionRecovery( consecutiveNonMutatingSuccessTools, runState.NoProgressRecoveryRetry)) return false; runState.NoProgressRecoveryRetry++; var latestFailure = TryGetLatestFailureSignal(messages); messages.Add(new ChatMessage { Role = "user", Content = BuildNoProgressExecutionRecoveryPrompt( taskPolicy, lastModifiedCodeFilePath, requireHighImpactCodeVerification, documentPlanPath, latestFailure.ToolName, latestFailure.Output) }); EmitEvent( AgentEventType.Thinking, "", $"?ㅽ뻾 吏꾩쟾???놁뼱 媛뺤젣 蹂듦뎄 ?④퀎瑜??쒖옉?⑸땲??({runState.NoProgressRecoveryRetry}/2)"); return true; } private static bool ShouldTriggerNoProgressExecutionRecovery( int consecutiveNonMutatingSuccessTools, int recoveryRetryCount) => consecutiveNonMutatingSuccessTools >= GetNoProgressRecoveryThreshold() && recoveryRetryCount < GetNoProgressRecoveryMaxRetries(); private static bool ShouldAbortNoProgressExecution( int consecutiveNonMutatingSuccessTools, int recoveryRetryCount) => consecutiveNonMutatingSuccessTools >= GetNoProgressAbortThreshold() && recoveryRetryCount >= GetNoProgressRecoveryMaxRetries(); private static string BuildNoProgressExecutionRecoveryPrompt( TaskTypePolicy taskPolicy, string? lastModifiedCodeFilePath, bool highImpactChange, string? documentPlanPath, string? recentFailureToolName, string? recentFailureOutput) { if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) { var fileHint = string.IsNullOrWhiteSpace(documentPlanPath) ? "寃곌낵 臾몄꽌 ?뚯씪" : $"'{documentPlanPath}'"; return "[System:NoProgressExecutionRecovery] ?쎄린/遺꾩꽍留?諛섎났?섍퀬 ?덉뒿?덈떎. " + $"?댁젣 ?ㅻ챸??硫덉텛怨?{fileHint}???ㅼ젣濡??앹꽦?섎뒗 ?꾧뎄(html_create/markdown_create/document_assemble/file_write)瑜?利됱떆 ?몄텧?섏꽭??"; } return BuildFailureNextToolPriorityPrompt( string.IsNullOrWhiteSpace(recentFailureToolName) ? "stagnation_guard" : recentFailureToolName!, lastModifiedCodeFilePath, highImpactChange, taskPolicy, recentFailureOutput); } private static (string? ToolName, string? Output) TryGetLatestFailureSignal(List messages) { foreach (var message in messages.AsEnumerable().Reverse()) { if (!TryGetToolResultInfo(message, out var toolName, out var content)) continue; if (IsLikelyFailureContent(content)) return (toolName, content); } return (null, null); } private static bool IsLikelyFailureContent(string? content) { if (string.IsNullOrWhiteSpace(content)) return false; var lower = content.ToLowerInvariant(); if (ContainsAny(lower, "success", "succeeded", "passed", "?듦낵", "?깃났", "鍮뚮뱶?덉뒿?덈떎", "tests passed", "build succeeded") && !ContainsAny(lower, "fail", "failed", "error", "?ㅻ쪟", "?ㅽ뙣", "exception", "denied", "not found")) return false; return ContainsAny( lower, "fail", "failed", "error", "exception", "timeout", "timed out", "denied", "forbidden", "not found", "invalid", "?ㅽ뙣", "?ㅻ쪟", "?덉쇅", "?쒓컙 珥덇낵", "沅뚰븳", "李⑤떒", "찾을 수"); } private static int UpdateConsecutiveReadOnlySuccessTools( int current, string toolName, bool success) { if (!success) return 0; return ReadOnlyTools.Contains(toolName ?? "") ? current + 1 : 0; } private static int UpdateConsecutiveNonMutatingSuccessTools( int current, string toolName, bool success) { if (!success) return 0; return IsMutatingOrExecutionProgressTool(toolName) ? 0 : current + 1; } private static bool IsMutatingOrExecutionProgressTool(string toolName) { return toolName is "file_write" or "file_edit" or "file_manage" or "html_create" or "markdown_create" or "docx_create" or "excel_create" or "csv_create" or "pptx_create" or "chart_create" or "document_assemble" or "document_plan" or "build_run" or "test_loop" or "process" or "git_tool"; } private static bool ShouldBlockNoProgressReadOnlyLoop( string toolName, int repeatedSameSignatureCount) { if (repeatedSameSignatureCount < GetReadOnlySignatureLoopThreshold()) return false; return ReadOnlyTools.Contains(toolName ?? ""); } private static int GetReadOnlySignatureLoopThreshold() { return ResolveConfiguredOrEnvThresholdValue( GetConfiguredThresholdFromSettings(llm => llm.ReadOnlySignatureLoopThreshold, 2, 12), Environment.GetEnvironmentVariable("AXCOPILOT_READONLY_SIGNATURE_LOOP_THRESHOLD"), 4, 2, 12); } private static int GetReadOnlyStagnationThreshold() { return ResolveConfiguredOrEnvThresholdValue( GetConfiguredThresholdFromSettings(llm => llm.ReadOnlyStagnationThreshold, 3, 20), Environment.GetEnvironmentVariable("AXCOPILOT_READONLY_STAGNATION_THRESHOLD"), 6, 3, 20); } private static int GetNoProgressRecoveryThreshold() { return ResolveConfiguredOrEnvThresholdValue( GetConfiguredThresholdFromSettings(llm => llm.NoProgressRecoveryThreshold, 4, 30), Environment.GetEnvironmentVariable("AXCOPILOT_NOPROGRESS_RECOVERY_THRESHOLD"), 8, 4, 30); } private static int GetNoProgressAbortThreshold() { return ResolveConfiguredOrEnvThresholdValue( GetConfiguredThresholdFromSettings(llm => llm.NoProgressAbortThreshold, 6, 50), Environment.GetEnvironmentVariable("AXCOPILOT_NOPROGRESS_ABORT_THRESHOLD"), 12, 6, 50); } private static int GetNoProgressRecoveryMaxRetries() { return ResolveConfiguredOrEnvThresholdValue( GetConfiguredThresholdFromSettings(llm => llm.NoProgressRecoveryMaxRetries, 1, 5), Environment.GetEnvironmentVariable("AXCOPILOT_NOPROGRESS_RECOVERY_MAX_RETRIES"), 2, 1, 5); } private static int ResolveConfiguredOrEnvThresholdValue( int? configured, string? envRaw, int defaultValue, int min, int max) { if (configured.HasValue) return configured.Value; return ResolveThresholdValue(envRaw, defaultValue, min, max); } private static int ResolveThresholdValue(string? raw, int defaultValue, int min, int max) { if (!int.TryParse(raw, out var value)) return defaultValue; return Math.Clamp(value, min, max); } private static int? GetConfiguredThresholdFromSettings(Func selector, int min, int max) { try { var app = System.Windows.Application.Current as App; var llm = app?.SettingsService?.Settings?.Llm; if (llm == null) return null; var configured = selector(llm); if (configured <= 0) return null; return Math.Clamp(configured, min, max); } catch { return null; } } private static string BuildNoProgressLoopRecoveryPrompt( string toolName, string? lastModifiedCodeFilePath, bool highImpactChange, TaskTypePolicy taskPolicy) { if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) { return "[System:NoProgressRecovery] ?숈씪???쎄린 ?몄텧??諛섎났?섏뿀?듬땲?? " + "?댁젣 臾몄꽌 ?앹꽦/?섏젙 ?꾧뎄(html_create/markdown_create/document_assemble/file_write) 以??섎굹瑜??ㅼ젣濡??몄텧??吏꾪뻾?섏꽭?? " + "?ㅻ챸留??섏? 留먭퀬 利됱떆 ?꾧뎄瑜??몄텧?섏꽭??"; } return BuildFailureNextToolPriorityPrompt( toolName, lastModifiedCodeFilePath, highImpactChange, taskPolicy); } private async Task TryHandleTransientLlmErrorTransitionAsync( Exception ex, RunState runState, CancellationToken ct) { if (!IsTransientLlmError(ex) || runState.TransientLlmErrorRetries >= 3) return false; runState.TransientLlmErrorRetries++; var delayMs = ComputeTransientLlmBackoffDelayMs(runState.TransientLlmErrorRetries, ex); EmitEvent( AgentEventType.Thinking, "", $"?쇱떆??LLM ?ㅻ쪟濡??ъ떆?꾪빀?덈떎 ({runState.TransientLlmErrorRetries}/3, {delayMs}ms ?€湲?"); await Task.Delay(delayMs, ct); return true; } private static bool IsTransientLlmError(Exception ex) { var message = ex.Message ?? ""; var lower = message.ToLowerInvariant(); if (ex is TaskCanceledException or TimeoutException or System.Net.Http.HttpRequestException) return true; return lower.Contains("429") || lower.Contains("rate limit") || lower.Contains("too many requests") || lower.Contains("timeout") || lower.Contains("timed out") || lower.Contains("temporarily unavailable") || lower.Contains("service unavailable") || lower.Contains("503") || lower.Contains("502") || lower.Contains("bad gateway") || lower.Contains("gateway timeout") || lower.Contains("connection reset") || lower.Contains("overloaded") || lower.Contains("try again"); } private static int ComputeTransientLlmBackoffDelayMs(int retryCount, Exception ex) { var message = ex.Message ?? ""; var retryAfterMatch = System.Text.RegularExpressions.Regex.Match( message, @"retry[- ]?after\s*[:=]?\s*(\d+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); if (retryAfterMatch.Success && int.TryParse(retryAfterMatch.Groups[1].Value, out var retryAfterSec)) return Math.Clamp(retryAfterSec * 1000, 500, 15000); var exponential = retryCount switch { 1 => 800, 2 => 1600, _ => 3200 }; var jitter = Random.Shared.Next(0, 250); return exponential + jitter; } private static int GetToolExecutionTimeoutMs() { var fromSettings = 0; try { var app = System.Windows.Application.Current as App; fromSettings = app?.SettingsService?.Settings?.Llm?.ToolExecutionTimeoutMs ?? 0; } catch { fromSettings = 0; } return ResolveToolExecutionTimeoutMs( fromSettings, Environment.GetEnvironmentVariable("AXCOPILOT_TOOL_TIMEOUT_MS")); } private static int ResolveToolExecutionTimeoutMs(int configuredMs, string? envRaw) { if (configuredMs > 0) return Math.Clamp(configuredMs, 5000, 600000); if (int.TryParse(envRaw, out var parsed) && parsed > 0) return Math.Clamp(parsed, 5000, 600000); return 90000; } private async Task ExecuteToolWithTimeoutAsync( IAgentTool tool, string toolName, JsonElement input, AgentContext context, List? messages, CancellationToken ct) { var timeoutMs = GetToolExecutionTimeoutMs(); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); timeoutCts.CancelAfter(timeoutMs); try { return await EnforceToolPermissionAsync(toolName, input, context, messages) ?? await tool.ExecuteAsync(input, context, timeoutCts.Token); } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return ToolResult.Fail($"?꾧뎄 ?ㅽ뻾 ?€?꾩븘??({timeoutMs}ms): {toolName}"); } } private async Task> SendWithToolsWithRecoveryAsync( List messages, IReadOnlyCollection tools, CancellationToken ct, string phaseLabel, RunState? runState = null) { var transientRetries = runState?.TransientLlmErrorRetries ?? 0; var contextRecoveryRetries = runState?.ContextRecoveryAttempts ?? 0; while (true) { try { return await _llm.SendWithToolsAsync(messages, tools, ct); } catch (Exception ex) { if (IsContextOverflowError(ex.Message) && contextRecoveryRetries < 2 && ForceContextRecovery(messages)) { contextRecoveryRetries++; if (runState != null) runState.ContextRecoveryAttempts = contextRecoveryRetries; EmitEvent( AgentEventType.Thinking, "", $"{phaseLabel}: 而⑦뀓?ㅽ듃 珥덇낵瑜?媛먯???蹂듦뎄 ???ъ떆?꾪빀?덈떎 ({contextRecoveryRetries}/2)"); continue; } if (IsTransientLlmError(ex) && transientRetries < 3) { transientRetries++; if (runState != null) runState.TransientLlmErrorRetries = transientRetries; var delayMs = ComputeTransientLlmBackoffDelayMs(transientRetries, ex); EmitEvent( AgentEventType.Thinking, "", $"{phaseLabel}: ?쇱떆??LLM ?ㅻ쪟濡??ъ떆?꾪빀?덈떎 ({transientRetries}/3, {delayMs}ms ?€湲?"); await Task.Delay(delayMs, ct); continue; } throw; } } } private void ApplyToolPostExecutionBookkeeping( LlmService.ContentBlock call, ToolResult result, TokenUsage? tokenUsage, Models.LlmSettings llm, int baseMax, List statsUsedTools, ref int totalToolCalls, ref int maxIterations, ref int statsSuccessCount, ref int statsFailCount, ref int statsInputTokens, ref int statsOutputTokens) { if (result.Success) statsSuccessCount++; else statsFailCount++; statsInputTokens += tokenUsage?.PromptTokens ?? 0; statsOutputTokens += tokenUsage?.CompletionTokens ?? 0; if (!statsUsedTools.Contains(call.ToolName)) statsUsedTools.Add(call.ToolName); if (llm.EnableAuditLog) { AuditLogService.LogToolCall( _conversationId, ActiveTab ?? "", call.ToolName, call.ToolInput.ToString() ?? "", TruncateOutput(result.Output, 500), result.FilePath, result.Success); } totalToolCalls++; if (totalToolCalls > 15 && maxIterations < baseMax * 2) maxIterations = Math.Min(baseMax * 2, 50); if (call.ToolName == "test_loop" && result.Output.Contains("[AUTO_FIX:")) { var testFixMax = llm.MaxTestFixIterations > 0 ? llm.MaxTestFixIterations : 5; var testFixBudget = baseMax + testFixMax * 3; if (maxIterations < testFixBudget) maxIterations = Math.Min(testFixBudget, 60); } } private bool TryHandleToolFailureTransition( LlmService.ContentBlock call, ToolResult result, AgentContext context, TaskTypePolicy taskPolicy, List messages, int maxRetry, string? lastModifiedCodeFilePath, bool requireHighImpactCodeVerification, string toolCallSignature, ref int consecutiveErrors, ref bool recoveryPendingAfterFailure, ref string? lastFailedToolSignature, ref int repeatedFailedToolSignatureCount) { if (result.Success) return false; var failureState = ComputeFailureTransitionState( toolCallSignature, lastFailedToolSignature, repeatedFailedToolSignatureCount, consecutiveErrors, maxRetry); lastFailedToolSignature = failureState.LastFailedToolSignature; repeatedFailedToolSignatureCount = failureState.RepeatedFailedToolSignatureCount; consecutiveErrors = failureState.ConsecutiveErrors; recoveryPendingAfterFailure = true; RememberFailurePattern(call.ToolName, result, context, taskPolicy, lastModifiedCodeFilePath, consecutiveErrors, maxRetry); var nonRetriableFailure = IsNonRetriableToolFailure(result); if (failureState.CanRetry && !nonRetriableFailure) { var reflectionMsg = BuildFailureReflectionMessage( call.ToolName, result, consecutiveErrors, maxRetry, taskPolicy); messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, reflectionMsg)); if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase) && call.ToolName is "build_run" or "test_loop") { messages.Add(new ChatMessage { Role = "user", Content = BuildFailureInvestigationPrompt( call.ToolName, lastModifiedCodeFilePath, requireHighImpactCodeVerification, taskPolicy, result.Output) }); } if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) { messages.Add(new ChatMessage { Role = "user", Content = BuildFailureNextToolPriorityPrompt( call.ToolName, lastModifiedCodeFilePath, requireHighImpactCodeVerification, taskPolicy, result.Output) }); } EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: ?ㅽ뙣 遺꾩꽍 ???ъ떆??({consecutiveErrors}/{maxRetry})"); return true; } if (nonRetriableFailure) { messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, $"[NON_RETRIABLE_FAILURE] {TruncateOutput(result.Output, 500)}\n" + "Retrying the same tool call is unlikely to succeed. Switch to a different tool or approach and explain next action.")); if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) { messages.Add(new ChatMessage { Role = "user", Content = BuildFailureNextToolPriorityPrompt( call.ToolName, lastModifiedCodeFilePath, requireHighImpactCodeVerification, taskPolicy, result.Output) }); } EmitEvent(AgentEventType.Thinking, "", "鍮꾩옱?쒕룄 ?ㅽ뙣濡?遺꾨쪟?섏뼱 ?숈씪 ?몄텧 諛섎났??以묐떒?섍퀬 ?고쉶 ?꾨왂?쇰줈 ?꾪솚?⑸땲??"); return true; } messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, $"[FAILED after {maxRetry} retries] {TruncateOutput(result.Output, 500)}\n" + "Stop retrying this tool. Explain the error to the user and suggest alternative approaches.")); try { var app = System.Windows.Application.Current as App; var memSvc = app?.MemoryService; if (memSvc != null && (app?.SettingsService?.Settings.Llm.EnableAgentMemory ?? false)) { memSvc.Add("correction", $"?꾧뎄 '{call.ToolName}' 諛섎났 ?ㅽ뙣: {TruncateOutput(result.Output, 200)}", $"conv:{_conversationId}", context.WorkFolder); } } catch { } return true; } private static bool IsNonRetriableToolFailure(ToolResult result) { if (result.Success) return false; var output = result.Output ?? ""; var lower = output.ToLowerInvariant(); return lower.Contains("알 수 없는 도구") || lower.Contains("unknown tool") || lower.Contains("permission denied") || lower.Contains("권한") || lower.Contains("forbidden") || lower.Contains("unauthorized") || lower.Contains("invalid argument") || lower.Contains("잘못된 인자") || lower.Contains("schema validation") || lower.Contains("json parse"); } private void ApplyDocumentPlanSuccessTransitions( LlmService.ContentBlock call, ToolResult result, List messages, ref bool documentPlanCalled, ref string? documentPlanPath, ref string? documentPlanTitle, ref string? documentPlanScaffold) { if (!string.Equals(call.ToolName, "document_plan", StringComparison.OrdinalIgnoreCase)) return; documentPlanCalled = true; var po = result.Output ?? string.Empty; var pm = System.Text.RegularExpressions.Regex.Match(po, @"path:\s*""([^""]+)"""); if (pm.Success) documentPlanPath = pm.Groups[1].Value; var tm = System.Text.RegularExpressions.Regex.Match(po, @"title:\s*""([^""]+)"""); if (tm.Success) documentPlanTitle = tm.Groups[1].Value; documentPlanScaffold = ExtractDocumentPlanScaffold(po); if (!ContainsDocumentPlanFollowUpInstruction(po)) return; var toolHint = ResolveDocumentPlanFollowUpTool(po); messages.Add(new ChatMessage { Role = "user", Content = "document_plan이 완료되었습니다. " + "방금 생성된 골격의 [내용...] 자리와 각 섹션 내용을 실제 상세 본문으로 모두 채운 뒤 " + $"{toolHint} 도구를 지금 즉시 호출하세요. " + "설명만 하지 말고 실제 문서 생성 도구 호출로 바로 이어가세요." }); EmitEvent(AgentEventType.Thinking, "", $"문서 개요 완료 · {toolHint} 실행 유도"); } private static string? ExtractDocumentPlanScaffold(string output) { if (string.IsNullOrWhiteSpace(output)) return null; var markers = new (string Start, string End)[] { ("--- body 시작 ---", "--- body 끝 ---"), ("--- body start ---", "--- body end ---"), ("", ""), }; foreach (var (startMarker, endMarker) in markers) { var start = output.IndexOf(startMarker, StringComparison.OrdinalIgnoreCase); if (start < 0) continue; var contentStart = start + startMarker.Length; var end = output.IndexOf(endMarker, contentStart, StringComparison.OrdinalIgnoreCase); if (end <= contentStart) continue; var scaffold = output[contentStart..end].Trim(); if (!string.IsNullOrWhiteSpace(scaffold)) return scaffold; } return null; } private static bool ContainsDocumentPlanFollowUpInstruction(string output) { if (string.IsNullOrWhiteSpace(output)) return false; return output.Contains("즉시 실행", StringComparison.OrdinalIgnoreCase) || output.Contains("immediate next step", StringComparison.OrdinalIgnoreCase) || output.Contains("call html_create", StringComparison.OrdinalIgnoreCase) || output.Contains("call document_assemble", StringComparison.OrdinalIgnoreCase); } private static string ResolveDocumentPlanFollowUpTool(string output) { if (output.Contains("document_assemble", StringComparison.OrdinalIgnoreCase)) return "document_assemble"; if (output.Contains("docx_create", StringComparison.OrdinalIgnoreCase)) return "docx_create"; if (output.Contains("markdown_create", StringComparison.OrdinalIgnoreCase)) return "markdown_create"; if (output.Contains("file_write", StringComparison.OrdinalIgnoreCase)) return "file_write"; return "html_create"; } private void ApplyCodeQualityFollowUpTransition( LlmService.ContentBlock call, ToolResult result, List messages, TaskTypePolicy taskPolicy, ref bool requireHighImpactCodeVerification, ref string? lastModifiedCodeFilePath) { var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result); if (ShouldInjectCodeQualityFollowUp(ActiveTab ?? "", call.ToolName, result)) { requireHighImpactCodeVerification = highImpactCodeChange; 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 async Task<(bool Completed, bool ConsumedExtraIteration)> TryHandleTerminalDocumentCompletionTransitionAsync( LlmService.ContentBlock call, ToolResult result, List toolCalls, List messages, Models.LlmSettings llm, AgentContext context, CancellationToken ct) { if (!result.Success || !IsTerminalDocumentTool(call.ToolName) || toolCalls.Count != 1) return (false, false); var verificationEnabled = AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm); var shouldVerify = ShouldRunPostToolVerification( ActiveTab, call.ToolName, result.Success, verificationEnabled, verificationEnabled); var consumedExtraIteration = false; if (shouldVerify) { await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); consumedExtraIteration = true; } EmitEvent(AgentEventType.Complete, "", "?먯씠?꾪듃 ?묒뾽 ?꾨즺"); return (true, consumedExtraIteration); } private async Task TryApplyPostToolVerificationTransitionAsync( LlmService.ContentBlock call, ToolResult result, List messages, Models.LlmSettings llm, AgentContext context, CancellationToken ct) { if (!result.Success) return false; var verificationEnabled = AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm); var shouldVerify = ShouldRunPostToolVerification( ActiveTab, call.ToolName, result.Success, verificationEnabled, verificationEnabled); if (!shouldVerify) return false; await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); return true; } private async Task<(bool ShouldContinue, string? TerminalResponse)> TryHandleUserDecisionTransitionsAsync( LlmService.ContentBlock call, AgentContext context, List messages) { if (context.DevModeStepApproval && UserDecisionCallback != null) { var decision = await UserDecisionCallback( $"[DEV] ?꾧뎄 '{call.ToolName}' ?ㅽ뻾???뱀씤?섏떆寃좎뒿?덇퉴?\n{FormatToolCallSummary(call)}", new List { "?뱀씤", "嫄대꼫?곌린", "以묐떒" }); var (devShouldContinue, devTerminalResponse, devToolResultMessage) = EvaluateDevStepDecision(decision); if (!string.IsNullOrEmpty(devTerminalResponse)) { EmitEvent(AgentEventType.Complete, "", "[DEV] ?ъ슜?먭? ?ㅽ뻾??以묐떒?덉뒿?덈떎"); return (false, devTerminalResponse); } if (devShouldContinue) { messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] ?ъ슜?먭? ???꾧뎄 ?ㅽ뻾??嫄대꼫?곗뿀?듬땲??")); return (true, null); } } var decisionRequired = CheckDecisionRequired(call, context); if (decisionRequired != null && UserDecisionCallback != null) { var decision = await UserDecisionCallback( decisionRequired, new List { "?뱀씤", "嫄대꼫?곌린", "痍⑥냼" }); var (scopeShouldContinue, scopeTerminalResponse, scopeToolResultMessage) = EvaluateScopeDecision(decision); if (!string.IsNullOrEmpty(scopeTerminalResponse)) { EmitEvent(AgentEventType.Complete, "", "?ъ슜?먭? ?묒뾽??痍⑥냼?덉뒿?덈떎"); return (false, scopeTerminalResponse); } if (scopeShouldContinue) { messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] ?ъ슜?먭? ???묒뾽??嫄대꼫?곗뿀?듬땲??")); return (true, null); } } return (false, null); } private sealed class RunState { public int ContextRecoveryAttempts; public int WithheldRecoveryAttempts; public int NoToolCallLoopRetry; public int CodeDiffGateRetry; public int RecentExecutionGateRetry; public int ExecutionSuccessGateRetry; public int HighImpactBuildTestGateRetry; public int CodeVerificationGateRetry; public int FinalReportGateRetry; public int TransientLlmErrorRetries; public int DocumentArtifactGateRetry; public int DocumentVerificationGateRetry; public int NoProgressRecoveryRetry; public int TerminalEvidenceGateRetry; public bool PendingPostCompactionTurn; public int PostCompactionTurnCounter; public string LastCompactionStageSummary = ""; public int LastCompactionSavedTokens; public int PostCompactionToolResultCompactions; } }