AX Agent 진행 시간·글로우 경로 정리 및 최근 로컬 변경 일괄 반영
- AX Agent 스트리밍 경과 시간을 공용 helper로 통일해 비정상적인 수천만 시간 표시를 방지함 - 채팅 입력창 글로우를 런처와 같은 표시/숨김 중심의 얇은 외곽 글로우로 정리하고 런처 글로우 설정은 일반 설정에 유지함 - README와 DEVELOPMENT 문서를 2026-04-08 12:02 (KST) 기준으로 갱신하고 Release 빌드 경고 0 / 오류 0을 확인함
This commit is contained in:
195
src/AxCopilot/Views/ChatWindow.AgentEventProcessor.cs
Normal file
195
src/AxCopilot/Views/ChatWindow.AgentEventProcessor.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System.Threading.Channels;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트 이벤트의 무거운 작업(대화 변이·저장)을 백그라운드 스레드에서 처리합니다.
|
||||
/// UI 스레드는 시각적 피드백만 담당하여 채팅창 멈춤을 방지합니다.
|
||||
/// </summary>
|
||||
public partial class ChatWindow
|
||||
{
|
||||
/// <summary>백그라운드 처리 대상 이벤트 큐 아이템.</summary>
|
||||
private readonly record struct AgentEventWorkItem(
|
||||
AgentEvent Event,
|
||||
string EventTab,
|
||||
string ActiveTab,
|
||||
bool ShouldRender);
|
||||
|
||||
private readonly Channel<AgentEventWorkItem> _agentEventChannel =
|
||||
Channel.CreateUnbounded<AgentEventWorkItem>(
|
||||
new UnboundedChannelOptions { SingleReader = true, AllowSynchronousContinuations = false });
|
||||
|
||||
private Task? _agentEventProcessorTask;
|
||||
|
||||
/// <summary>백그라운드 이벤트 프로세서를 시작합니다. 창 초기화 시 한 번 호출됩니다.</summary>
|
||||
private void StartAgentEventProcessor()
|
||||
{
|
||||
_agentEventProcessorTask = Task.Run(ProcessAgentEventsAsync);
|
||||
}
|
||||
|
||||
/// <summary>백그라운드 이벤트 프로세서를 종료합니다. 창 닫기 시 호출됩니다.</summary>
|
||||
private void StopAgentEventProcessor()
|
||||
{
|
||||
_agentEventChannel.Writer.TryComplete();
|
||||
// 프로세서 완료를 동기 대기하지 않음 — 데드락 방지
|
||||
// GC가 나머지를 정리합니다.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트 이벤트를 백그라운드 큐에 추가합니다.
|
||||
/// 대화 변이(AppendExecutionEvent, AppendAgentRun)와 디스크 저장을 백그라운드에서 처리합니다.
|
||||
/// </summary>
|
||||
private void EnqueueAgentEventWork(AgentEvent evt, string eventTab, bool shouldRender)
|
||||
{
|
||||
_agentEventChannel.Writer.TryWrite(new AgentEventWorkItem(evt, eventTab, _activeTab, shouldRender));
|
||||
}
|
||||
|
||||
/// <summary>백그라운드 전용: 채널에서 이벤트를 배치로 읽어 대화 변이 + 저장을 수행합니다.</summary>
|
||||
private async Task ProcessAgentEventsAsync()
|
||||
{
|
||||
var reader = _agentEventChannel.Reader;
|
||||
var persistStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
ChatConversation? pendingPersist = null;
|
||||
var batch = new List<AgentEventWorkItem>(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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user