모델별 time-based compact 기준과 compact 메타 노출을 경량화

- service:model 조합별로 time-based tool_result 정리 기준을 분리해 Claude는 보수적으로, Qwen/vLLM 계열은 빠르게 오래된 결과를 걷어내도록 조정
- compact 메타 카드를 제목과 한 줄 요약 중심으로 단순화해 transcript 운영 노이즈를 축소
- README와 DEVELOPMENT 문서에 2026-04-12 22:19 (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 21:53:05 +09:00
parent bdd4444deb
commit ef58e93e38
4 changed files with 109 additions and 39 deletions

View File

@@ -41,6 +41,16 @@ public partial class ChatWindow
&& string.IsNullOrWhiteSpace(msg.Content))
return false;
// 에이전트 루프 내부 메시지 (도구 호출 블록 / 도구 결과) — 채팅 타임라인에 표시하지 않음
var content = msg.Content;
if (content != null)
{
if (msg.Role == "assistant" && content.StartsWith("{\"_tool_use_blocks\""))
return false;
if (msg.Role == "user" && content.StartsWith("{\"type\":\"tool_result\""))
return false;
}
return true;
}).ToList() ?? new List<ChatMessage>();
@@ -78,6 +88,11 @@ public partial class ChatWindow
private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent)
{
var restoredEvent = ToAgentEvent(executionEvent);
// Claude 스타일: SessionStart / UserPromptSubmit 운영 이벤트는 항상 숨김
if (restoredEvent.Type == AgentEventType.SessionStart || restoredEvent.Type == AgentEventType.UserPromptSubmit)
return false;
if (restoredEvent.Type == AgentEventType.Complete || restoredEvent.Type == AgentEventType.Error)
return true;
@@ -141,6 +156,21 @@ public partial class ChatWindow
var eventIndex = 0;
string? prevToolCallName = null;
int consecutiveToolCallCount = 0;
// 과거 대화(비-스트리밍) 히스토리: 연속 process feed 이벤트를 접힌 요약 그룹으로 묶기
var pendingProcessFeedGroup = new List<AgentEvent>();
DateTime pendingGroupTimestamp = default;
void FlushProcessFeedGroup()
{
if (pendingProcessFeedGroup.Count == 0) return;
var capturedGroup = pendingProcessFeedGroup.ToList();
var ts = pendingGroupTimestamp;
var groupKey = $"eg_{ts.Ticks}_{eventIndex++}";
timeline.Add((groupKey, ts, 1, () => AddCollapsedProcessFeedGroup(capturedGroup)));
pendingProcessFeedGroup.Clear();
}
foreach (var executionEvent in visibleEvents)
{
// 스트리밍 중이고 히스토리 접힘 상태일 때, 현재 run의 process feed 이벤트는 통합 카드에서 표시
@@ -164,7 +194,6 @@ public partial class ChatWindow
consecutiveToolCallCount++;
continue; // 연속 중복 스킵
}
// 이전 연속 카운트가 있었으면 이전 pill에 반영됨
prevToolCallName = restoredEvent.ToolName;
consecutiveToolCallCount = 1;
}
@@ -174,9 +203,22 @@ public partial class ChatWindow
consecutiveToolCallCount = 0;
}
// 과거 대화(비-스트리밍)에서 process feed 이벤트를 그룹으로 묶기
if (!_isStreaming && IsProcessFeedEvent(restoredEvent))
{
pendingProcessFeedGroup.Add(restoredEvent);
pendingGroupTimestamp = executionEvent.Timestamp;
continue;
}
// 비-process feed 이벤트가 오면 이전 그룹 플러시
FlushProcessFeedGroup();
var eventKey = $"e_{executionEvent.Timestamp.Ticks}_{eventIndex++}";
timeline.Add((eventKey, executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
}
// 마지막 그룹 플러시
FlushProcessFeedGroup();
// 스트리밍 중 + 히스토리 접힘: 통합 진행 카드 삽입 (개별 pill 대체)
if (!showFullHistory && _isStreaming && _currentRunProgressSteps.Count > 0)
@@ -187,9 +229,10 @@ public partial class ChatWindow
timeline.Add(("_live_progress", cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps)));
}
var liveProgressHint = GetLiveAgentProgressHint();
if (liveProgressHint != null)
timeline.Add(("_live_hint", liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint)));
// 라이브 프로그레스 힌트는 트랜스크립트에 표시하지 않음
// 입력창 위의 PulseDotBar로만 상태를 표시 (Claude Desktop 스타일)
// 이전에는 "처리 중..." / "작업을 준비하는 중입니다..." 등이
// 트랜스크립트에 불필요하게 표시되어 사용자 경험을 저해함
// 대부분 이미 시간순이므로 정렬 필요 여부를 먼저 확인 — O(n) 스캔으로 O(n log n) 정렬 회피
var needsSort = false;
@@ -237,7 +280,7 @@ public partial class ChatWindow
new TextBlock
{
Text = "\uE70D",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontFamily = s_segoeIconFont,
FontSize = 8,
Foreground = secondaryText,
Margin = new Thickness(0, 0, 4, 0),
@@ -312,7 +355,7 @@ public partial class ChatWindow
{
"session_memory_compaction" => "세션 메모리 압축",
"collapsed_boundary" => "압축 경계 병합",
_ => "Microcompact 경계",
_ => "컨텍스트 정리",
};
var wrapper = new Border
@@ -332,7 +375,7 @@ public partial class ChatWindow
header.Children.Add(new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontFamily = s_segoeIconFont,
FontSize = 11,
Foreground = accentBrush,
VerticalAlignment = VerticalAlignment.Center,
@@ -348,34 +391,20 @@ public partial class ChatWindow
});
stack.Children.Add(header);
var lines = (message.Content ?? "").Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim()).Where(line => !string.IsNullOrWhiteSpace(line)).ToList();
foreach (var line in lines)
var summary = message.MetaKind switch
{
var isHeaderLine = line.StartsWith("[", StringComparison.Ordinal);
stack.Children.Add(new TextBlock
{
Text = isHeaderLine ? line.Trim('[', ']') : line,
FontSize = 10.5,
FontWeight = isHeaderLine ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = isHeaderLine ? primaryText : secondaryText,
TextWrapping = TextWrapping.Wrap,
Margin = isHeaderLine ? new Thickness(0, 0, 0, 3) : new Thickness(0, 0, 0, 2),
});
}
"session_memory_compaction" => "이전 요약과 실행 경계를 하나의 세션 메모로 정리했습니다.",
"collapsed_boundary" => "이전 compact 경계를 합쳐 transcript를 더 가볍게 유지했습니다.",
_ => "오래된 실행/도구 결과를 줄여 다음 요청 컨텍스트를 가볍게 만들었습니다.",
};
if (!string.IsNullOrWhiteSpace(message.MetaRunId))
stack.Children.Add(new TextBlock
{
stack.Children.Add(new TextBlock
{
Text = $"run {message.MetaRunId}",
FontSize = 9.5,
Foreground = secondaryText,
Opacity = 0.7,
Margin = new Thickness(0, 6, 0, 0),
});
}
Text = summary,
FontSize = 10.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
});
wrapper.Child = stack;
return wrapper;