AX Agent loop 정책을 분리하고 transcript 가상화·성능 계측 구조를 강화한다
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- AgentLoop 검증/문서/compact 정책 메서드를 partial 파일로 분리해 loop 본문의 책임을 줄임 - transcript 렌더에 cache pruning과 deferred scrolling을 적용해 긴 세션의 UI 부담을 낮춤 - AgentPerformanceLogService를 추가해 transcript 렌더와 agent loop 실행 요약을 perf 로그로 남김 - README와 DEVELOPMENT 문서에 2026-04-09 10:08 (KST) 기준 구조 개선 및 계측 이력을 반영함 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 검증 결과 경고 0개 오류 0개
This commit is contained in:
@@ -156,6 +156,7 @@ public partial class AgentLoopService
|
||||
{
|
||||
if (IsRunning) throw new InvalidOperationException("에이전트가 이미 실행 중입니다.");
|
||||
|
||||
var runStopwatch = Stopwatch.StartNew();
|
||||
IsRunning = true;
|
||||
_currentRunId = Guid.NewGuid().ToString("N");
|
||||
_docFallbackAttempted = false;
|
||||
@@ -1741,6 +1742,30 @@ public partial class AgentLoopService
|
||||
EmitEvent(AgentEventType.StepDone, "total_stats", summary);
|
||||
}
|
||||
}
|
||||
|
||||
runStopwatch.Stop();
|
||||
AgentPerformanceLogService.LogMetric(
|
||||
"agent_loop",
|
||||
"run_summary",
|
||||
_conversationId,
|
||||
ActiveTab ?? "",
|
||||
runStopwatch.ElapsedMilliseconds,
|
||||
new
|
||||
{
|
||||
runId = _currentRunId,
|
||||
iterations = iteration,
|
||||
toolCalls = totalToolCalls,
|
||||
toolSuccess = statsSuccessCount,
|
||||
toolFail = statsFailCount,
|
||||
inputTokens = statsInputTokens,
|
||||
outputTokens = statsOutputTokens,
|
||||
repeatedFailureBlocks = statsRepeatedFailureBlocks,
|
||||
recoveredAfterFailure = statsRecoveredAfterFailure,
|
||||
postCompactionNoiseSuppressed = _runPostCompactionSuppressedThinkingCount,
|
||||
postCompactionToolResultCompactions = _runPostCompactionToolResultCompactions,
|
||||
taskType = taskPolicy.TaskType,
|
||||
model = _settings.Settings.Llm.Model ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4619,118 +4644,6 @@ public partial class AgentLoopService
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkRunPostCompaction(ContextCompactionResult result, RunState runState)
|
||||
{
|
||||
runState.PendingPostCompactionTurn = true;
|
||||
runState.PostCompactionTurnCounter = 0;
|
||||
runState.LastCompactionStageSummary = result.StageSummary;
|
||||
runState.LastCompactionSavedTokens = result.SavedTokens;
|
||||
SyncRunPostCompactionState(runState);
|
||||
}
|
||||
|
||||
private void NotifyPostCompactionTurnIfNeeded(RunState runState)
|
||||
{
|
||||
if (!runState.PendingPostCompactionTurn)
|
||||
return;
|
||||
|
||||
runState.PendingPostCompactionTurn = false;
|
||||
runState.PostCompactionTurnCounter++;
|
||||
SyncRunPostCompactionState(runState);
|
||||
|
||||
var stage = string.IsNullOrWhiteSpace(runState.LastCompactionStageSummary)
|
||||
? "기본"
|
||||
: runState.LastCompactionStageSummary;
|
||||
var saved = runState.LastCompactionSavedTokens > 0
|
||||
? $" · {Services.TokenEstimator.Format(runState.LastCompactionSavedTokens)} tokens 절감"
|
||||
: "";
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"compact 이후 {runState.PostCompactionTurnCounter}번째 턴 · {stage}{saved}");
|
||||
}
|
||||
|
||||
private void SyncRunPostCompactionState(RunState runState)
|
||||
{
|
||||
_runPendingPostCompactionTurn = runState.PendingPostCompactionTurn;
|
||||
_runPostCompactionTurnCounter = runState.PostCompactionTurnCounter;
|
||||
_runLastCompactionStageSummary = runState.LastCompactionStageSummary ?? "";
|
||||
_runLastCompactionSavedTokens = runState.LastCompactionSavedTokens;
|
||||
_runPostCompactionToolResultCompactions = runState.PostCompactionToolResultCompactions;
|
||||
}
|
||||
|
||||
private bool ShouldSuppressPostCompactionThinking(string summary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(summary))
|
||||
return false;
|
||||
|
||||
var postCompactActive = _runPendingPostCompactionTurn || _runPostCompactionTurnCounter > 0;
|
||||
if (!postCompactActive)
|
||||
return false;
|
||||
|
||||
if (_runPostCompactionTurnCounter > 1)
|
||||
return false;
|
||||
|
||||
return summary.StartsWith("LLM에 요청 중", StringComparison.Ordinal)
|
||||
|| summary.StartsWith("사용자 프롬프트 제출", StringComparison.Ordinal)
|
||||
|| summary.StartsWith("무료 티어 모드", StringComparison.Ordinal)
|
||||
|| summary.StartsWith("컨텍스트 압축 완료", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string BuildLoopToolResultMessage(LlmService.ContentBlock call, ToolResult result, RunState runState)
|
||||
{
|
||||
var output = result.Output ?? "";
|
||||
if (!ShouldCompactToolResultForPostCompactionTurn(runState, call.ToolName, output))
|
||||
return TruncateOutput(output, 4000);
|
||||
|
||||
runState.PostCompactionToolResultCompactions++;
|
||||
SyncRunPostCompactionState(runState);
|
||||
|
||||
var compacted = CompactToolResultForPostCompaction(call.ToolName, output);
|
||||
var stage = string.IsNullOrWhiteSpace(runState.LastCompactionStageSummary)
|
||||
? "compact"
|
||||
: runState.LastCompactionStageSummary;
|
||||
return $"[POST_COMPACTION_TOOL_RESULT:{call.ToolName}] {stage}\n{compacted}";
|
||||
}
|
||||
|
||||
private static bool ShouldCompactToolResultForPostCompactionTurn(RunState runState, string toolName, string output)
|
||||
{
|
||||
if (runState.PostCompactionTurnCounter != 1)
|
||||
return false;
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
return false;
|
||||
if (output.Length < 900)
|
||||
return false;
|
||||
|
||||
return toolName is "process" or "build_run" or "test_loop" or "snippet_runner" or "git_tool" or "http_tool"
|
||||
|| output.Length > 1800;
|
||||
}
|
||||
|
||||
private static string CompactToolResultForPostCompaction(string toolName, string output)
|
||||
{
|
||||
var normalized = output.Replace("\r\n", "\n");
|
||||
var lines = normalized.Split('\n', StringSplitOptions.None)
|
||||
.Select(line => line.TrimEnd())
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||
.ToList();
|
||||
|
||||
if (lines.Count == 0)
|
||||
return TruncateOutput(output, 1200);
|
||||
|
||||
var headCount = toolName is "build_run" or "test_loop" or "process" ? 6 : 4;
|
||||
var tailCount = toolName is "build_run" or "test_loop" or "process" ? 4 : 3;
|
||||
var kept = new List<string>();
|
||||
kept.AddRange(lines.Take(headCount));
|
||||
|
||||
if (lines.Count > headCount + tailCount)
|
||||
kept.Add($"...[post-compact snip: {lines.Count - headCount - tailCount:N0}줄 생략]...");
|
||||
|
||||
if (lines.Count > headCount)
|
||||
kept.AddRange(lines.Skip(Math.Max(headCount, lines.Count - tailCount)));
|
||||
|
||||
var compacted = string.Join("\n", kept.Distinct(StringComparer.Ordinal));
|
||||
return TruncateOutput(compacted, 1400);
|
||||
}
|
||||
|
||||
/// <summary>영향 범위 기반 의사결정 체크. 확인이 필요하면 메시지를 반환, 불필요하면 null.</summary>
|
||||
private string? CheckDecisionRequired(LlmService.ContentBlock call, AgentContext context)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user