using System.Diagnostics; using AxCopilot.Models; namespace AxCopilot.Services.Agent; public partial class AgentLoopService { private sealed record AgentLoopRunBootstrap( Stopwatch RunStopwatch, string UserQuery, int MaxIterations, int MaxRetry, IntentResult IntentResult, ExplorationTrackingState ExplorationState, PathAccessTrackingState PathAccessState, SessionLearningCollector? SessionLearnings, TaskTypePolicy TaskPolicy, ModelExecutionProfileCatalog.ExecutionPolicy ExecutionPolicy); private sealed record AgentLoopRunFinalizerState( string RunId, Stopwatch RunStopwatch, DateTime StatsStart, ExplorationTrackingState ExplorationState, TaskTypePolicy TaskPolicy, int Iteration, int TotalToolCalls, int StatsSuccessCount, int StatsFailCount, int StatsInputTokens, int StatsOutputTokens, int StatsRepeatedFailureBlocks, int StatsRecoveredAfterFailure, IReadOnlyList StatsUsedTools, IReadOnlyDictionary FailedToolHistogram); private (Stopwatch RunStopwatch, string UserQuery, int MaxIterations, int MaxRetry) BeginRun(List messages) { var runStopwatch = Stopwatch.StartNew(); IsRunning = true; _currentRunId = Guid.NewGuid().ToString("N"); _docFallbackAttempted = false; _documentPlanApproved = false; _pendingCommands.Clear(); var llm = _settings.Settings.Llm; var baseMax = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25; var maxRetry = llm.MaxRetryOnError > 0 ? llm.MaxRetryOnError : 3; var userQuery = messages.LastOrDefault(m => m.Role == "user")?.Content ?? ""; return (runStopwatch, userQuery, baseMax, maxRetry); } private async Task BootstrapRunAsync( List messages, Stopwatch runStopwatch, string userQuery, int maxIterations, int maxRetry, CancellationToken ct) { var intentGate = new IntentGateService(_llm); var intentResult = await intentGate.ClassifyAsync(userQuery, ActiveTab, ct).ConfigureAwait(false); var explorationState = new ExplorationTrackingState { Scope = intentResult.SuggestedScope, SelectiveHit = true, }; var pathAccessState = new PathAccessTrackingState(); var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings) ? new SessionLearningCollector(_settings.Settings.Llm.MaxSessionLearnings) : null; var taskPolicy = TaskTypePolicy.FromTaskType(intentResult.TaskType); var executionPolicy = ExecutionPolicyMerger.Apply( _llm.GetActiveExecutionPolicy(), intentResult.PolicyOverlay); maxRetry = ComputeAdaptiveMaxRetry(maxRetry, taskPolicy.TaskType); var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType); maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType); InjectTaskTypeGuidance(messages, taskPolicy); InjectExplorationScopeGuidance(messages, explorationState.Scope); if (intentResult.IsComplexTask && !string.IsNullOrWhiteSpace(intentResult.DecompositionHint)) { messages.Add(new ChatMessage { Role = "user", Content = $"[System:DecompositionHint]\n{intentResult.DecompositionHint}\n" + "Consider using spawn_agents to run independent sub-tasks in parallel.", MetaKind = "decomposition_hint", }); } return new AgentLoopRunBootstrap( runStopwatch, userQuery, maxIterations, maxRetry, intentResult, explorationState, pathAccessState, sessionLearnings, taskPolicy, executionPolicy); } private void FinalizeRun(AgentLoopRunFinalizerState state) { var compactNoiseSuppressed = _runPostCompactionSuppressedThinkingCount; var compactedToolResultCount = _runPostCompactionToolResultCompactions; WorkflowLogService.LogAgentLifecycle( _conversationId, state.RunId, "end", summary: $"iterations={state.Iteration}, tools={state.TotalToolCalls}, success={state.StatsSuccessCount}, fail={state.StatsFailCount}"); if (state.TotalToolCalls > 0) { AgentPerformanceLogService.LogExplorationBreadth( _conversationId, ActiveTab, new { scope = state.ExplorationState.Scope.ToString().ToLowerInvariant(), folder_map_calls = state.ExplorationState.FolderMapCalls, total_files_read = state.ExplorationState.TotalFilesRead, multi_read_files = state.ExplorationState.MultiReadFilesRead, broad_scan = state.ExplorationState.BroadScanDetected, selective_hit = state.ExplorationState.SelectiveHit, corrective_hint = state.ExplorationState.CorrectiveHintInjected }); var durationMs = (long)(DateTime.Now - state.StatsStart).TotalMilliseconds; AgentStatsService.RecordSession(new AgentStatsService.AgentSessionRecord { Timestamp = state.StatsStart, Tab = ActiveTab ?? "", TaskType = state.TaskPolicy.TaskType, Model = _settings.Settings.Llm.Model ?? "", ToolCalls = state.TotalToolCalls, SuccessCount = state.StatsSuccessCount, FailCount = state.StatsFailCount, InputTokens = state.StatsInputTokens, OutputTokens = state.StatsOutputTokens, DurationMs = durationMs, RepeatedFailureBlockedCount = state.StatsRepeatedFailureBlocks, RecoveredAfterFailureCount = state.StatsRecoveredAfterFailure, UsedTools = [.. state.StatsUsedTools], }); var llm = _settings.Settings.Llm; if (llm.ShowTotalCallStats) { var totalTokens = state.StatsInputTokens + state.StatsOutputTokens; var durationSec = durationMs / 1000.0; var toolList = string.Join(", ", state.StatsUsedTools); var retryTotal = state.StatsRepeatedFailureBlocks + state.StatsRecoveredAfterFailure; var retryQuality = retryTotal > 0 ? $"{(state.StatsRecoveredAfterFailure * 100.0 / retryTotal):F0}%" : "100%"; var compactNoiseSummary = compactNoiseSuppressed > 0 ? $" | compact 로그 축약 {compactNoiseSuppressed}건" : ""; var compactToolResultSummary = compactedToolResultCount > 0 ? $" | compact 결과 축약 {compactedToolResultCount}건" : ""; var topFailed = BuildTopFailureSummary(new Dictionary(state.FailedToolHistogram, StringComparer.OrdinalIgnoreCase)); var summary = $"📊 전체 통계: LLM {state.Iteration}회 호출 | 도구 {state.TotalToolCalls}회 (성공 {state.StatsSuccessCount}, 실패 {state.StatsFailCount}) | " + $"토큰 {state.StatsInputTokens:N0}→{state.StatsOutputTokens:N0} (합계 {totalTokens:N0}) | " + $"소요 {durationSec:F1}초 | 재시도 품질 {retryQuality} (복구 {state.StatsRecoveredAfterFailure}, 차단 {state.StatsRepeatedFailureBlocks}){compactNoiseSummary}{compactToolResultSummary} | " + $"실패 상위: {topFailed} | 사용 도구: {toolList}"; EmitEvent(AgentEventType.StepDone, "total_stats", summary); } } state.RunStopwatch.Stop(); AgentPerformanceLogService.LogMetric( "agent_loop", "run_summary", _conversationId, ActiveTab ?? "", state.RunStopwatch.ElapsedMilliseconds, new { runId = state.RunId, iterations = state.Iteration, toolCalls = state.TotalToolCalls, toolSuccess = state.StatsSuccessCount, toolFail = state.StatsFailCount, inputTokens = state.StatsInputTokens, outputTokens = state.StatsOutputTokens, repeatedFailureBlocks = state.StatsRepeatedFailureBlocks, recoveredAfterFailure = state.StatsRecoveredAfterFailure, postCompactionNoiseSuppressed = compactNoiseSuppressed, postCompactionToolResultCompactions = compactedToolResultCount, taskType = state.TaskPolicy.TaskType, model = _settings.Settings.Llm.Model ?? "", }); ResetRunTransientState(); } private void ResetRunTransientState() { IsRunning = false; _currentRunId = ""; _runPendingPostCompactionTurn = false; _runPostCompactionTurnCounter = 0; _runPostCompactionSuppressedThinkingCount = 0; _runLastCompactionStageSummary = ""; _runLastCompactionSavedTokens = 0; _runPostCompactionToolResultCompactions = 0; if (IsPaused) { IsPaused = false; try { _pauseSemaphore.Release(); } catch (SemaphoreFullException) { } } } }