에이전트 루프 수명주기와 tool_result 복원 품질을 마감한다
목적: - AgentLoopService 시작/종료 책임을 분리해 루프 본체를 더 얇은 orchestration 구조로 정리한다. - 장기 세션과 분기 대화에서 tool_result preview가 다른 tool_use_id로 재등장해도 안정적으로 복원되도록 replacement state를 보강한다. - DOCX/PPTX/XLSX 반환 경로까지 문서 품질 출력 formatter를 통일해 포맷별 quality summary와 repair guide 표현을 일관되게 맞춘다. 핵심 수정: - AgentLoopRunLifecycle.cs를 추가해 BeginRun/BootstrapRunAsync/FinalizeRun/ResetRunTransientState를 분리하고, AgentLoopService는 해당 helper를 사용하도록 정리했다. - run 종료 metric에 빈 run id가 기록되던 흐름을 수정해 실제 run id가 유지되도록 고쳤다. - AgentMessageInvariantHelper에 persisted preview map, fingerprint preview map, tool_use_id 재바인딩 로직을 추가해 저장/재개/분기 이후 preview 복원 품질을 높였다. - ArtifactQualityOutputFormatter를 DocxSkill, PptxSkill, ExcelSkill 멀티시트 출력 경로까지 연결해 quality summary/repair guide 문자열 조립을 공통 helper로 통일했다. - AgentMessageInvariantHelperTests에 fingerprint 재바인딩 회귀를 추가했다. - README.md, docs/DEVELOPMENT.md, docs/NEXT_ROADMAP.md에 2026-04-15 10:05 (KST) 기준 변경 이력을 반영했다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_run_finalize\\ -p:IntermediateOutputPath=obj\\verify_run_finalize\\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentCommandQueueTests|AgentQueuedCommandProjectorTests|AgentMessageInvariantHelperTests|AgentQueryContextBuilderTests|ChatStorageServiceTests|PptxSkillGoldenDeckTests|ExcelSkillGoldenWorkbookTests|DocxSkillGoldenDocumentTests|HtmlSkillGoldenReportTests -p:OutputPath=bin\\verify_run_finalize_tests2\\ -p:IntermediateOutputPath=obj\\verify_run_finalize_tests2\\ : 통과 18
This commit is contained in:
@@ -233,34 +233,21 @@ public partial class AgentLoopService
|
||||
{
|
||||
if (IsRunning) throw new InvalidOperationException("에이전트가 이미 실행 중입니다.");
|
||||
|
||||
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 maxIterations = baseMax; // 동적 조정 가능
|
||||
var maxRetry = llm.MaxRetryOnError > 0 ? llm.MaxRetryOnError : 3;
|
||||
var iteration = 0;
|
||||
|
||||
// 사용자 원본 요청 캡처 (문서 생성 폴백 판단용)
|
||||
var userQuery = messages.LastOrDefault(m => m.Role == "user")?.Content ?? "";
|
||||
// IntentGate: 통합 의도 분류
|
||||
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();
|
||||
// P3: 누적 학습 — 도구 결과에서 자동 학습 포인트 수집
|
||||
var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings)
|
||||
? new SessionLearningCollector(_settings.Settings.Llm.MaxSessionLearnings)
|
||||
: null;
|
||||
var bootstrap = BeginRun(messages);
|
||||
var runStopwatch = bootstrap.RunStopwatch;
|
||||
var userQuery = bootstrap.UserQuery;
|
||||
var maxIterations = bootstrap.MaxIterations;
|
||||
var baseMax = bootstrap.MaxIterations;
|
||||
var maxRetry = bootstrap.MaxRetry;
|
||||
var runBootstrap = await BootstrapRunAsync(messages, runStopwatch, userQuery, maxIterations, maxRetry, ct).ConfigureAwait(false);
|
||||
maxIterations = runBootstrap.MaxIterations;
|
||||
maxRetry = runBootstrap.MaxRetry;
|
||||
var intentResult = runBootstrap.IntentResult;
|
||||
var explorationState = runBootstrap.ExplorationState;
|
||||
var pathAccessState = runBootstrap.PathAccessState;
|
||||
var sessionLearnings = runBootstrap.SessionLearnings;
|
||||
DateTime? lastToolResultAtUtc = null;
|
||||
string? lastToolResultToolName = null;
|
||||
|
||||
@@ -304,9 +291,8 @@ public partial class AgentLoopService
|
||||
string? lastArtifactFilePath = null; // 생성/수정 산출물 파일 추적
|
||||
var statsModifiedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // 수정/생성된 파일 추적
|
||||
var taskType = intentResult.TaskType;
|
||||
var taskPolicy = TaskTypePolicy.FromTaskType(taskType);
|
||||
var executionPolicy = ExecutionPolicyMerger.Apply(
|
||||
_llm.GetActiveExecutionPolicy(), intentResult.PolicyOverlay);
|
||||
var taskPolicy = runBootstrap.TaskPolicy;
|
||||
var executionPolicy = runBootstrap.ExecutionPolicy;
|
||||
var consecutiveNoToolResponses = 0;
|
||||
var noToolResponseThreshold = AgentLoopRuntimeThresholds.GetNoToolCallResponseThreshold(executionPolicy.NoToolResponseThreshold);
|
||||
var noToolRecoveryMaxRetries = AgentLoopRuntimeThresholds.GetNoToolCallRecoveryMaxRetries(executionPolicy.NoToolRecoveryMaxRetries);
|
||||
@@ -317,25 +303,8 @@ public partial class AgentLoopService
|
||||
var runState = new RunState();
|
||||
var requireHighImpactCodeVerification = false;
|
||||
string? lastModifiedCodeFilePath = null;
|
||||
maxRetry = ComputeAdaptiveMaxRetry(maxRetry, taskPolicy.TaskType);
|
||||
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
|
||||
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
||||
|
||||
var context = BuildContext();
|
||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
||||
|
||||
// P5: 복합 요청 감지 시 DecompositionHint 주입
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||
explorationState,
|
||||
@@ -1869,112 +1838,23 @@ public partial class AgentLoopService
|
||||
{
|
||||
if (runtimeOverrideApplied)
|
||||
_llm.PopInferenceOverride();
|
||||
|
||||
// 워크플로우 상세 로그: 에이전트 루프 종료
|
||||
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "end",
|
||||
summary: $"iterations={iteration}, tools={totalToolCalls}, success={statsSuccessCount}, fail={statsFailCount}");
|
||||
|
||||
IsRunning = false;
|
||||
_currentRunId = "";
|
||||
_runPendingPostCompactionTurn = false;
|
||||
_runPostCompactionTurnCounter = 0;
|
||||
_runPostCompactionSuppressedThinkingCount = 0;
|
||||
_runLastCompactionStageSummary = "";
|
||||
_runLastCompactionSavedTokens = 0;
|
||||
_runPostCompactionToolResultCompactions = 0;
|
||||
|
||||
// 일시정지 상태 리셋
|
||||
if (IsPaused)
|
||||
{
|
||||
IsPaused = false;
|
||||
try { _pauseSemaphore.Release(); }
|
||||
catch (SemaphoreFullException) { }
|
||||
}
|
||||
|
||||
// 통계 기록 (도구 호출이 1회 이상인 세션만)
|
||||
if (totalToolCalls > 0)
|
||||
{
|
||||
AgentPerformanceLogService.LogExplorationBreadth(
|
||||
_conversationId,
|
||||
ActiveTab,
|
||||
new
|
||||
{
|
||||
scope = explorationState.Scope.ToString().ToLowerInvariant(),
|
||||
folder_map_calls = explorationState.FolderMapCalls,
|
||||
total_files_read = explorationState.TotalFilesRead,
|
||||
multi_read_files = explorationState.MultiReadFilesRead,
|
||||
broad_scan = explorationState.BroadScanDetected,
|
||||
selective_hit = explorationState.SelectiveHit,
|
||||
corrective_hint = explorationState.CorrectiveHintInjected
|
||||
});
|
||||
|
||||
var durationMs = (long)(DateTime.Now - statsStart).TotalMilliseconds;
|
||||
|
||||
AgentStatsService.RecordSession(new AgentStatsService.AgentSessionRecord
|
||||
{
|
||||
Timestamp = statsStart,
|
||||
Tab = ActiveTab ?? "",
|
||||
TaskType = taskPolicy.TaskType,
|
||||
Model = _settings.Settings.Llm.Model ?? "",
|
||||
ToolCalls = totalToolCalls,
|
||||
SuccessCount = statsSuccessCount,
|
||||
FailCount = statsFailCount,
|
||||
InputTokens = statsInputTokens,
|
||||
OutputTokens = statsOutputTokens,
|
||||
DurationMs = durationMs,
|
||||
RepeatedFailureBlockedCount = statsRepeatedFailureBlocks,
|
||||
RecoveredAfterFailureCount = statsRecoveredAfterFailure,
|
||||
UsedTools = statsUsedTools,
|
||||
});
|
||||
|
||||
// 전체 호출·토큰 합계 표시 (개발자 모드 설정)
|
||||
if (llm.ShowTotalCallStats)
|
||||
{
|
||||
var totalTokens = statsInputTokens + statsOutputTokens;
|
||||
var durationSec = durationMs / 1000.0;
|
||||
var toolList = string.Join(", ", statsUsedTools);
|
||||
var retryTotal = statsRepeatedFailureBlocks + statsRecoveredAfterFailure;
|
||||
var retryQuality = retryTotal > 0
|
||||
? $"{(statsRecoveredAfterFailure * 100.0 / retryTotal):F0}%"
|
||||
: "100%";
|
||||
var compactNoiseSummary = _runPostCompactionSuppressedThinkingCount > 0
|
||||
? $" | compact 로그 축약 {_runPostCompactionSuppressedThinkingCount}건"
|
||||
: "";
|
||||
var compactToolResultSummary = _runPostCompactionToolResultCompactions > 0
|
||||
? $" | compact 결과 축약 {_runPostCompactionToolResultCompactions}건"
|
||||
: "";
|
||||
var topFailed = BuildTopFailureSummary(failedToolHistogram);
|
||||
var summary = $"📊 전체 통계: LLM {iteration}회 호출 | 도구 {totalToolCalls}회 (성공 {statsSuccessCount}, 실패 {statsFailCount}) | " +
|
||||
$"토큰 {statsInputTokens:N0}→{statsOutputTokens:N0} (합계 {totalTokens:N0}) | " +
|
||||
$"소요 {durationSec:F1}초 | 재시도 품질 {retryQuality} (복구 {statsRecoveredAfterFailure}, 차단 {statsRepeatedFailureBlocks}){compactNoiseSummary}{compactToolResultSummary} | " +
|
||||
$"실패 상위: {topFailed} | 사용 도구: {toolList}";
|
||||
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 ?? "",
|
||||
});
|
||||
var completedRunId = _currentRunId;
|
||||
FinalizeRun(new AgentLoopRunFinalizerState(
|
||||
completedRunId,
|
||||
runStopwatch,
|
||||
statsStart,
|
||||
explorationState,
|
||||
taskPolicy,
|
||||
iteration,
|
||||
totalToolCalls,
|
||||
statsSuccessCount,
|
||||
statsFailCount,
|
||||
statsInputTokens,
|
||||
statsOutputTokens,
|
||||
statsRepeatedFailureBlocks,
|
||||
statsRecoveredAfterFailure,
|
||||
statsUsedTools,
|
||||
failedToolHistogram));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user