Some checks failed
Release Gate / gate (push) Has been cancelled
- claude-code 선택적 탐색 흐름을 참고해 Cowork/Code 시스템 프롬프트에서 folder_map 상시 선행 지시를 완화하고 glob/grep 기반 좁은 탐색을 우선하도록 조정함 - FolderMapTool 기본 depth를 2로, include_files 기본값을 false로 낮추고 MultiReadTool 최대 파일 수를 8개로 줄여 초기 과탐색 폭을 보수적으로 조정함 - AgentLoopExplorationPolicy partial을 추가해 탐색 범위 분류, broad-scan corrective hint, exploration_breadth 성능 로그를 연결함 - AgentLoopService에 탐색 범위 가이드 주입과 실행 중 탐색 폭 추적을 추가하고, 좁은 질문에서 반복적인 folder_map/대량 multi_read를 교정하도록 정리함 - DocxToHtmlConverter nullable 경고를 수정해 Release 빌드 경고 0 / 오류 0 기준을 다시 충족함 - README와 docs/DEVELOPMENT.md에 2026-04-09 10:36 (KST) 기준 개발 이력을 반영함
216 lines
9.1 KiB
C#
216 lines
9.1 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>
|
|
/// 에이전트 이벤트를 백그라운드 큐에 추가합니다.
|
|
/// ExecutionEvents는 UI 스레드에서 즉시 추가 (타임라인 렌더링 누락 방지).
|
|
/// 디스크 저장과 AgentRun 기록은 백그라운드에서 처리합니다.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <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 { }
|
|
}
|
|
}
|
|
}
|