에이전트 선택적 탐색 구조 개선과 경고 정리 반영
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) 기준 개발 이력을 반영함
This commit is contained in:
2026-04-09 14:27:59 +09:00
parent 7931566212
commit 33c1db4dae
119 changed files with 4453 additions and 6943 deletions

View File

@@ -1,7 +1,81 @@
using System.Collections.Generic;
namespace AxCopilot.Views;
public partial class ChatWindow
{
/// <summary>
/// B-3: 스트리밍 전용 append-only 렌더 경로.
/// prefix 비교를 완전히 우회하고, stable 키의 부분집합 관계만 확인하여
/// 새 항목만 추가하는 빠른 경로. B-1/B-2로 인크리멘탈이 대부분 성공하지만,
/// 스트리밍 중 키 순서가 변경되는 극단적 경우에도 전체 재빌드를 방지합니다.
/// </summary>
private bool TryApplyStreamingAppendRender(TranscriptRenderPlan renderPlan)
{
if (!_isStreaming || _lastRenderedTimelineKeys.Count == 0 || renderPlan.NewKeys.Count == 0)
return false;
// hiddenCount가 다르면 visible 범위 자체가 달라진 것 — append 불가
if (renderPlan.HiddenCount != _lastRenderedHiddenCount)
return false;
// 기존 stable 키가 새 키 집합의 부분집합인지 확인 (순서 무관)
var previousStable = new HashSet<string>(StringComparer.Ordinal);
foreach (var key in _lastRenderedTimelineKeys)
{
if (!key.StartsWith("_live_", StringComparison.Ordinal))
previousStable.Add(key);
}
var newStableSet = new HashSet<string>(StringComparer.Ordinal);
foreach (var key in renderPlan.NewKeys)
{
if (!key.StartsWith("_live_", StringComparison.Ordinal))
newStableSet.Add(key);
}
if (!previousStable.IsSubsetOf(newStableSet))
return false;
try
{
// 라이브 컨테이너 임시 분리
var hadLiveContainer = _agentLiveContainer != null && ContainsTranscriptElement(_agentLiveContainer);
if (hadLiveContainer)
RemoveTranscriptElement(_agentLiveContainer!);
// 기존 live 항목 제거 (끝에서부터)
for (var i = _lastRenderedTimelineKeys.Count - 1; i >= 0; i--)
{
if (!_lastRenderedTimelineKeys[i].StartsWith("_live_", StringComparison.Ordinal))
break;
if (GetTranscriptElementCount() > 0)
RemoveTranscriptElementAt(GetTranscriptElementCount() - 1);
}
// 새로 추가된 항목만 렌더 (기존에 없던 키)
foreach (var item in renderPlan.VisibleTimeline)
{
if (!previousStable.Contains(item.Key))
item.Render();
}
// 라이브 컨테이너 재삽입
if (hadLiveContainer && _agentLiveContainer != null)
AddTranscriptElement(_agentLiveContainer);
_lastRenderedTimelineKeys = renderPlan.NewKeys;
_lastRenderedHiddenCount = renderPlan.HiddenCount;
return true;
}
catch (Exception ex)
{
Services.LogService.Warn($"스트리밍 append 렌더 실패, 전체 렌더로 전환: {ex.Message}");
_lastRenderedTimelineKeys.Clear();
return false;
}
}
private bool TryApplyIncrementalTranscriptRender(TranscriptRenderPlan renderPlan)
{
if (!renderPlan.CanIncremental)
@@ -22,12 +96,21 @@ public partial class ChatWindow
try
{
// B-2: 라이브 컨테이너가 transcript 끝에 있으면 임시 분리
var hadLiveContainer = _agentLiveContainer != null && ContainsTranscriptElement(_agentLiveContainer);
if (hadLiveContainer)
RemoveTranscriptElement(_agentLiveContainer!);
for (var removeIndex = 0; removeIndex < renderPlan.PreviousLiveCount && GetTranscriptElementCount() > 0; removeIndex++)
RemoveTranscriptElementAt(GetTranscriptElementCount() - 1);
for (var i = renderPlan.PreviousStableCount; i < renderPlan.VisibleTimeline.Count; i++)
renderPlan.VisibleTimeline[i].Render();
// B-2: 라이브 컨테이너 재삽입
if (hadLiveContainer && _agentLiveContainer != null)
AddTranscriptElement(_agentLiveContainer);
_lastRenderedTimelineKeys = renderPlan.NewKeys;
_lastRenderedHiddenCount = renderPlan.HiddenCount;
return true;