- 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)
1571 lines
60 KiB
C#
1571 lines
60 KiB
C#
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;
|
||
}
|
||
}
|
||
|
||
|
||
|