compact 이후 복원 메모 계층화와 compact UI 메타 축소

- post_compact_context 메시지에 compact summary 수와 structured tool history 블록 수를 추가해 compact 뒤 첫 query turn의 복원 맥락을 더 명확히 전달함

- compact 메타 카드를 더 짧은 한 줄 요약과 파일 개수 중심으로 줄여 transcript에서 운영 메타 밀도를 낮춤

- 컨텍스트 사용 팝업의 compact 디테일을 짧은 한국어 표현으로 정리해 claw-code 스타일의 얇은 운영 표현에 가깝게 맞춤

- README.md 및 docs/DEVELOPMENT.md를 2026-04-12 23:23 (KST) 기준으로 갱신함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
This commit is contained in:
2026-04-12 22:22:18 +09:00
parent c7b2bba063
commit 58b798d3e4
5 changed files with 53 additions and 15 deletions

View File

@@ -153,11 +153,25 @@ public static class AgentQueryContextBuilder
.Take(5)
.ToList();
var imageCount = messages.Sum(m => m.Images?.Count ?? 0);
var compactSummaryCount = messages.Count(m =>
string.Equals(m.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|| string.Equals(m.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|| string.Equals(m.MetaKind, "collapsed_boundary", StringComparison.OrdinalIgnoreCase));
var structuredToolHistoryCount = messages.Count(m =>
{
var content = m.Content ?? "";
return content.StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal)
|| content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal);
});
if (attachedFiles.Count == 0 && imageCount == 0)
if (attachedFiles.Count == 0 && imageCount == 0 && compactSummaryCount == 0 && structuredToolHistoryCount == 0)
return;
var lines = new List<string> { "[post-compact context]" };
if (compactSummaryCount > 0)
lines.Add($"restored compact summaries: {compactSummaryCount}");
if (structuredToolHistoryCount > 0)
lines.Add($"restored tool history blocks: {structuredToolHistoryCount}");
if (attachedFiles.Count > 0)
lines.Add("restored file refs: " + string.Join(", ", attachedFiles));
if (imageCount > 0)

View File

@@ -6,7 +6,7 @@ namespace AxCopilot.Views;
public partial class ChatWindow
{
// 토큰 추정 캐시: 메시지 수/대화 ID가 바뀔 때만 재계산
// 토큰 추정 캐시: 메시지 수대화 ID가 달라질 때만 재계산
private int _cachedMessageTokens;
private int _cachedMessageCountForTokens = -1;
private string? _cachedConvIdForTokens;
@@ -32,8 +32,6 @@ public partial class ChatWindow
var triggerPercent = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95);
var triggerRatio = triggerPercent / 100.0;
// 메시지 토큰 추정: 메시지 수나 대화 ID가 바뀔 때만 재계산 (타이핑 중 반복 계산 방지)
// 스트리밍 중에는 매번 재계산 (도구 결과 메시지가 실시간으로 추가됨)
int messageTokens;
lock (_convLock)
{
@@ -53,8 +51,6 @@ public partial class ChatWindow
var draftText = InputBox?.Text ?? "";
var draftTokens = string.IsNullOrWhiteSpace(draftText) ? 0 : Services.TokenEstimator.Estimate(draftText) + 4;
// 시스템 프롬프트 + 도구 정의 오버헤드: 첫 메시지 전송 이후에만 포함
// 새 대화(메시지 0개)에서는 0% 표시 — 사용자 혼동 방지
var hasAnyMessages = messageTokens > 0 || _isStreaming;
int baseOverhead = 0;
if (hasAnyMessages)
@@ -63,6 +59,7 @@ public partial class ChatWindow
var toolCount = _toolRegistry?.GetActiveToolsForTab(_activeTab ?? "Chat")?.Count ?? 0;
baseOverhead = Services.TokenEstimator.EstimateBaseOverhead(sysPromptLen, toolCount);
}
var currentTokens = Math.Max(0, messageTokens + draftTokens + baseOverhead);
var usageRatio = Services.TokenEstimator.GetContextUsage(currentTokens, maxContextTokens);
@@ -80,7 +77,7 @@ public partial class ChatWindow
else if (usageRatio >= triggerRatio)
{
progressBrush = Brushes.DarkOrange;
summary = llm.EnableProactiveContextCompact ? "곧 자동 압축" : "압축 계 도달";
summary = llm.EnableProactiveContextCompact ? "곧 자동 압축" : "압축 계 도달";
compactLabel = "압축 권장";
}
else if (usageRatio >= triggerRatio * 0.7)
@@ -99,7 +96,7 @@ public partial class ChatWindow
if (_lastCompactionAt.HasValue && _lastCompactionBeforeTokens.HasValue && _lastCompactionAfterTokens.HasValue)
{
var compactType = _lastCompactionWasAutomatic ? "자동" : "수동";
detailText = $"{compactType} 압축 {Services.TokenEstimator.Format(_lastCompactionBeforeTokens.Value)} {Services.TokenEstimator.Format(_lastCompactionAfterTokens.Value)}";
detailText = $"{compactType} 압축 {Services.TokenEstimator.Format(_lastCompactionBeforeTokens.Value)} -> {Services.TokenEstimator.Format(_lastCompactionAfterTokens.Value)}";
}
else
{
@@ -107,7 +104,7 @@ public partial class ChatWindow
}
TokenUsageArc.Stroke = progressBrush;
var percentText = $"{Math.Round(usageRatio * 100):0}%";
var percentText = $"{System.Math.Round(usageRatio * 100):0}%";
TokenUsagePercentText.Text = percentText;
TokenUsageSummaryText.Text = $"컨텍스트 {percentText}";
TokenUsageHintText.Text = $"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}";
@@ -124,8 +121,8 @@ public partial class ChatWindow
TokenUsagePopupDetail.Text = detailText;
if (TokenUsagePopupCompact != null)
TokenUsagePopupCompact.Text = _sessionCompactionCount > 0
? $"누적 압축 {_sessionCompactionCount}회 · 절감 {FormatTokenCount(_sessionCompactionSavedTokens)} tokens"
: "AX Agent가 컨텍스트를 자동으로 관리합니다";
? $"누적 압축 {_sessionCompactionCount}회 · 절감 {FormatTokenCount(_sessionCompactionSavedTokens)}"
: "AX Agent가 컨텍스트를 자동 관리합니다";
TokenUsageCard.ToolTip = null;

View File

@@ -393,14 +393,21 @@ public partial class ChatWindow
var summary = message.MetaKind switch
{
"session_memory_compaction" => "이전 요약과 실행 경계를 하나의 세션 메모로 정리했습니다.",
"collapsed_boundary" => "이전 compact 경계를 합쳐 transcript를 더 가볍게 유지했습니다.",
_ => "오래된 실행/도구 결과를 줄여 다음 요청 컨텍스트를 가볍게 만들었습니다.",
"session_memory_compaction" => "이전 기록을 세션 메모로 정리했습니다.",
"collapsed_boundary" => "이전 압축 경계를 합쳤습니다.",
_ => "오래된 실행 기록을 줄였습니다.",
};
var detailBits = new List<string>();
if (message.AttachedFiles?.Count > 0)
detailBits.Add($"파일 {message.AttachedFiles.Count}개");
var compactDetail = detailBits.Count > 0
? $"{summary} · {string.Join(", ", detailBits)}"
: summary;
stack.Children.Add(new TextBlock
{
Text = summary,
Text = compactDetail,
FontSize = 10.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,