Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs
lacvet 9fafcd0192 코워크 문서 계획/생성 흐름을 claw-code 기준으로 복구
- document_plan 결과에서 body 골격과 후속 생성 도구를 안정적으로 추출하도록 AgentLoop 분기를 수정

- 코워크 문서형 작업은 planMode=off여도 계획 선행(always) 경로를 타도록 보정

- 코워크 시스템 프롬프트를 강화해 계획만 제시하고 끝나지 않고 실제 문서 파일 생성까지 이어지게 조정

- README와 DEVELOPMENT 문서에 2026-04-05 16:02 (KST) 기준 변경 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:05:18 +09:00

1571 lines
60 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using AxCopilot.Models;
using AxCopilot.Services;
using System.Text.Json;
namespace AxCopilot.Services.Agent;
public partial class AgentLoopService
{
private bool TryHandleContextOverflowTransition(
Exception ex,
List<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> messages)
{
var modificationTools = new HashSet<string>(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<ChatMessage> 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<ChatMessage> 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<ChatMessage> messages,
int verificationGateRetry)
{
return string.Equals(taskType, "docs", StringComparison.OrdinalIgnoreCase)
&& HasMaterializedArtifact(lastArtifactFilePath)
&& !HasDocumentVerificationEvidenceAfterLastArtifact(messages)
&& verificationGateRetry < 1;
}
private static bool HasDocumentVerificationEvidenceAfterLastArtifact(List<ChatMessage> messages)
{
var artifactTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"html_create", "markdown_create", "docx_create", "excel_create", "csv_create", "pptx_create",
"chart_create", "document_assemble", "file_write"
};
var verificationTools = new HashSet<string>(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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<ChatMessage> 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<LlmSettings, int> 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<bool> 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<ToolResult> ExecuteToolWithTimeoutAsync(
IAgentTool tool,
string toolName,
JsonElement input,
AgentContext context,
List<ChatMessage>? 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<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
List<ChatMessage> messages,
IReadOnlyCollection<IAgentTool> 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<string> 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<ChatMessage> 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<ChatMessage> 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 ---"),
("<!-- body start marker -->", "<!-- body end marker -->"),
};
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<ChatMessage> 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<LlmService.ContentBlock> toolCalls,
List<ChatMessage> 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<bool> TryApplyPostToolVerificationTransitionAsync(
LlmService.ContentBlock call,
ToolResult result,
List<ChatMessage> 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<ChatMessage> messages)
{
if (context.DevModeStepApproval && UserDecisionCallback != null)
{
var decision = await UserDecisionCallback(
$"[DEV] ?꾧뎄 '{call.ToolName}' ?ㅽ뻾???뱀씤?섏떆寃좎뒿?덇퉴?\n{FormatToolCallSummary(call)}",
new List<string> { "?뱀씤", "嫄대꼫?곌린", "以묐떒" });
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<string> { "?뱀씤", "嫄대꼫?곌린", "痍⑥냼" });
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;
}
}