- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 claude-code 동등 품질 기준으로 재정렬함 - OpenAI/vLLM 경로의 오래된 tool history를 평탄화하고 최근 이력만 구조화해 컨텍스트 직렬화를 경량화함 - AX Agent UI를 테마 기준으로 재구성하고 플랜 승인/오버레이/이벤트 렌더링/명령 입력 상호작용을 개선함 - 파일 후보 제안, 반복 경로 정체 복구, LSP 보강, 문서·PPT 처리 개선, 설정/서비스 인터페이스 정리를 함께 반영함 - README.md 및 docs/DEVELOPMENT.md를 작업 시점별로 갱신함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
234 lines
10 KiB
C#
234 lines
10 KiB
C#
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>
|
|
/// 에이전트 이벤트를 백그라운드 큐에 추가합니다.
|
|
/// 대화 히스토리 반영과 저장은 프로세서에서 한 번만 수행합니다.
|
|
/// </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;
|
|
|
|
try
|
|
{
|
|
lock (_convLock)
|
|
{
|
|
var session = _appState.ChatSession;
|
|
if (session != null)
|
|
{
|
|
var result = _chatEngine.AppendExecutionEvent(
|
|
session, _storage, _currentConversation, activeTab, eventTab, evt);
|
|
// 방어: 결과 대화가 빈 대화로 교체되는 것을 방지
|
|
// 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 result = _chatEngine.AppendAgentRun(
|
|
session, _storage, _currentConversation, activeTab, eventTab, evt, "completed", summary);
|
|
// 방어: 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 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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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
|
|
{
|
|
// 종료 중 마지막 저장 실패는 무시한다.
|
|
}
|
|
}
|
|
}
|
|
}
|