using System.Threading.Channels; using System.Windows; using System.Windows.Threading; using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; namespace AxCopilot.Views; /// /// 에이전트 이벤트의 무거운 작업(대화 반영, 저장, 실행 이력 기록)을 /// 백그라운드 단일 리더에서 배치 처리해 UI 스레드 점유를 줄입니다. /// public partial class ChatWindow { /// 백그라운드 처리 대상 에이전트 이벤트 단위입니다. private readonly record struct AgentEventWorkItem( AgentEvent Event, string EventTab, string ActiveTab, bool ShouldRender); private readonly Channel _agentEventChannel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleReader = true, AllowSynchronousContinuations = false }); private Task? _agentEventProcessorTask; /// 백그라운드 이벤트 프로세서를 시작합니다. private void StartAgentEventProcessor() { _agentEventProcessorTask = Task.Run(ProcessAgentEventsAsync); } /// 백그라운드 이벤트 프로세서를 종료합니다. private void StopAgentEventProcessor() { _agentEventChannel.Writer.TryComplete(); // 종료 대기는 생략한다. 남은 정리는 GC와 종료 루틴에 맡긴다. } /// /// 에이전트 이벤트를 백그라운드 큐에 추가합니다. /// 대화 히스토리 반영과 저장은 프로세서에서 한 번만 수행합니다. /// private void EnqueueAgentEventWork(AgentEvent evt, string eventTab, bool shouldRender) { _agentEventChannel.Writer.TryWrite(new AgentEventWorkItem(evt, eventTab, _activeTab, shouldRender)); } /// /// 백그라운드 전용: 채널에 쌓인 이벤트를 배치로 읽어 대화 반영과 저장을 수행합니다. /// private async Task ProcessAgentEventsAsync() { var reader = _agentEventChannel.Reader; var persistStopwatch = System.Diagnostics.Stopwatch.StartNew(); ChatConversation? pendingPersist = null; var batch = new List(16); try { while (await reader.WaitToReadAsync().ConfigureAwait(false)) { batch.Clear(); while (reader.TryRead(out var item)) batch.Add(item); if (batch.Count == 0) continue; bool anyNeedsRender = false; bool hasTerminalEvent = false; foreach (var work in batch) { var evt = work.Event; var eventTab = work.EventTab; var activeTab = work.ActiveTab; try { lock (_convLock) { var session = _appState.ChatSession; if (session != null) { var targetConversation = GetStreamingConversation(eventTab); var result = _chatEngine.AppendExecutionEvent( session, _storage, _currentConversation, activeTab, eventTab, evt, targetConversation); // 방어: 결과 대화가 빈 대화로 교체되는 것을 방지 // EnsureCurrentConversation이 기존 대화 대신 새 빈 대화를 생성하는 경우 발생 var resultConv = result.CurrentConversation; var currentMsgCount = _currentConversation?.Messages?.Count ?? 0; var resultMsgCount = resultConv?.Messages?.Count ?? 0; if (resultConv != null && resultMsgCount == 0 && currentMsgCount > 0 && _currentConversation != null && string.Equals( _currentConversation.Tab?.Trim(), resultConv.Tab?.Trim(), StringComparison.OrdinalIgnoreCase)) { // 기존 대화 유지 — session에도 복원 session.CurrentConversation = _currentConversation; LogService.Info($"[EventProc] 대화 교체 차단: 기존 msgCount={currentMsgCount}, 결과 msgCount={resultMsgCount}, convId={_currentConversation.Id?[..Math.Min(8, _currentConversation.Id?.Length ?? 0)]}"); } else { _currentConversation = resultConv; } pendingPersist = result.UpdatedConversation; } } } catch (Exception ex) { LogService.Debug($"백그라운드 이벤트 처리 오류 (execution): {ex.Message}"); } if (evt.Type == AgentEventType.Complete) { try { lock (_convLock) { var session = _appState.ChatSession; if (session != null) { var summary = string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary; var targetConversation = GetStreamingConversation(eventTab); var result = _chatEngine.AppendAgentRun( session, _storage, _currentConversation, activeTab, eventTab, evt, "completed", summary, targetConversation); // 방어: Complete 이벤트에서도 대화 교체 보호 var completeMsgCount = result.CurrentConversation?.Messages?.Count ?? 0; var existingMsgCount = _currentConversation?.Messages?.Count ?? 0; if (completeMsgCount == 0 && existingMsgCount > 0 && _currentConversation != null) { session.CurrentConversation = _currentConversation; } else { _currentConversation = result.CurrentConversation; } pendingPersist = result.UpdatedConversation; } } } catch (Exception ex) { LogService.Debug($"백그라운드 이벤트 처리 오류 (agent run complete): {ex.Message}"); } hasTerminalEvent = true; } else if (evt.Type == AgentEventType.Error && string.IsNullOrWhiteSpace(evt.ToolName)) { try { lock (_convLock) { var session = _appState.ChatSession; if (session != null) { var summary = string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 실행 실패" : evt.Summary; var targetConversation = GetStreamingConversation(eventTab); var result = _chatEngine.AppendAgentRun( session, _storage, _currentConversation, activeTab, eventTab, evt, "failed", summary, targetConversation); _currentConversation = result.CurrentConversation; pendingPersist = result.UpdatedConversation; } } } catch (Exception ex) { LogService.Debug($"백그라운드 이벤트 처리 오류 (agent run error): {ex.Message}"); } hasTerminalEvent = true; } if (work.ShouldRender) anyNeedsRender = true; } if (pendingPersist != null && (hasTerminalEvent || persistStopwatch.ElapsedMilliseconds > 2000)) { try { _storage.Save(pendingPersist); var rememberTab = pendingPersist.Tab ?? "Cowork"; var session = _appState.ChatSession; if (session?.CurrentConversation != null && string.Equals(session.CurrentConversation.Id, pendingPersist.Id, StringComparison.Ordinal)) { session.RememberConversation(rememberTab, pendingPersist.Id); } } catch (Exception ex) { LogService.Debug($"백그라운드 대화 저장 실패: {ex.Message}"); } pendingPersist = null; persistStopwatch.Restart(); } if (anyNeedsRender) { try { Application.Current?.Dispatcher?.BeginInvoke( () => ScheduleExecutionHistoryRender(autoScroll: true), DispatcherPriority.Background); } catch { // 창 종료 중에는 무시한다. } } } } catch (OperationCanceledException) { } catch (ChannelClosedException) { } catch (Exception ex) { LogService.Debug($"에이전트 이벤트 프로세서 종료: {ex.Message}"); } if (pendingPersist != null) { try { _storage.Save(pendingPersist); } catch { // 종료 중 마지막 저장 실패는 무시한다. } } } }