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가 나머지를 정리합니다. } /// /// 에이전트 이벤트를 백그라운드 큐에 추가합니다. /// ExecutionEvents는 UI 스레드에서 즉시 추가 (타임라인 렌더링 누락 방지). /// 디스크 저장과 AgentRun 기록은 백그라운드에서 처리합니다. /// private void EnqueueAgentEventWork(AgentEvent evt, string eventTab, bool shouldRender) { // ── 즉시 추가: ExecutionEvents에 동기적으로 반영 (RenderMessages 누락 방지) ── try { lock (_convLock) { var session = _appState.ChatSession; if (session != null) { var result = _chatEngine.AppendExecutionEvent( session, null!, _currentConversation, _activeTab, eventTab, evt); _currentConversation = result.CurrentConversation; } } } catch (Exception ex) { LogService.Debug($"UI 스레드 이벤트 즉시 추가 실패: {ex.Message}"); } _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; // ── 대화 변이: execution event 추가 ── try { lock (_convLock) { var session = _appState.ChatSession; if (session != null) { var result = _chatEngine.AppendExecutionEvent( session, _storage, _currentConversation, activeTab, eventTab, evt); _currentConversation = result.CurrentConversation; pendingPersist = result.UpdatedConversation; } } } catch (Exception ex) { LogService.Debug($"백그라운드 이벤트 처리 오류 (execution): {ex.Message}"); } // ── 대화 변이: agent run 추가 (Complete/Error) ── if (evt.Type == AgentEventType.Complete) { try { lock (_convLock) { var session = _appState.ChatSession; if (session != null) { var summary = string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary; var result = _chatEngine.AppendAgentRun( session, _storage, _currentConversation, activeTab, eventTab, evt, "completed", summary); _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 result = _chatEngine.AppendAgentRun( session, _storage, _currentConversation, activeTab, eventTab, evt, "failed", summary); _currentConversation = result.CurrentConversation; pendingPersist = result.UpdatedConversation; } } } catch (Exception ex) { LogService.Debug($"백그라운드 이벤트 처리 오류 (agent run error): {ex.Message}"); } hasTerminalEvent = true; } if (work.ShouldRender) anyNeedsRender = true; } // ── 디바운스 저장: 2초마다 또는 종료 이벤트 시 즉시 ── if (pendingPersist != null && (hasTerminalEvent || persistStopwatch.ElapsedMilliseconds > 2000)) { try { _storage.Save(pendingPersist); var rememberTab = pendingPersist.Tab ?? "Cowork"; _appState.ChatSession?.RememberConversation(rememberTab, pendingPersist.Id); } catch (Exception ex) { LogService.Debug($"백그라운드 대화 저장 실패: {ex.Message}"); } pendingPersist = null; persistStopwatch.Restart(); } // ── UI 새로고침 신호: 배치 전체에 대해 단 1회 ── 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 { } } } }