From ac8e9f96860db02091f1ff4f81d10e92fd831832 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 23:38:56 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=95=95=EC=B6=95=203=EB=8B=A8=EA=B3=84=20=EC=9D=B4=EC=8B=9D?= =?UTF-8?q?=EA=B3=BC=20microcompact=20=EA=B2=BD=EA=B3=84=20=EC=9A=94?= =?UTF-8?q?=EC=95=BD=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - claude-code compact 흐름을 참고해 AX ContextCondenser를 도구 결과 축약 -> microcompact -> 이전 대화 요약 3단계로 확장함 - 오래된 실행 로그, tool_result, 메타 이벤트, 과도하게 긴 메시지를 microcompact_boundary로 먼저 압축해 LLM 요약 전 토큰을 덜어내도록 보강함 - README 및 개발 문서/로드맵에 2026-04-04 23:32 (KST) 기준 이력을 반영함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0) --- README.md | 5 + docs/AGENT_ROADMAP.md | 5 +- docs/DEVELOPMENT.md | 6 +- .../Services/Agent/ContextCondenser.cs | 183 +++++++++++++++++- 4 files changed, 191 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4b2aca7..57b8cf9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 +- 업데이트: 2026-04-04 23:32 (KST) +- AX Agent 컨텍스트 압축 경로에 `microcompact` 성격의 선행 경량 압축 단계를 추가해, 오래된 실행 로그·도구 결과·긴 메시지를 먼저 경계 요약으로 줄인 뒤 LLM 요약 단계로 넘기도록 보강했습니다. +- 수동 `/compact`와 자동 압축이 같은 `ContextCondenser` 3단계 흐름(도구 결과 축약 → microcompact → 이전 대화 요약)을 사용하도록 정리해 긴 세션에서 불필요한 토큰 사용을 더 줄이기 시작했습니다. +- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` + - 업데이트: 2026-04-04 23:14 (KST) - 트레이 아이콘 우클릭 메뉴 상단에 `AX Copilot v.0.7.3` 버전 헤더를 추가하고, 좌클릭 시에는 런처보다 AX Agent를 우선 열도록 정리했습니다. AI 기능이 꺼져 있을 때만 기존처럼 런처를 열도록 유지했습니다. - Enter/전송 버튼/슬래시 명령의 DraftQueue kind 분류를 다시 정리해 일반 입력은 `message`, 슬래시 입력은 `command`, 조정 입력은 `steering`, 직접 실행 요청은 `direct`로 더 자연스럽게 나뉘도록 보강했습니다. diff --git a/docs/AGENT_ROADMAP.md b/docs/AGENT_ROADMAP.md index 6555038..031d1fc 100644 --- a/docs/AGENT_ROADMAP.md +++ b/docs/AGENT_ROADMAP.md @@ -1,4 +1,4 @@ -# AX Agent 로드맵 (전면 재작성) +# AX Agent 로드맵 (전면 재작성) ## 1. 보존 이력 (요약만 유지) - v0.5.0: MCP 연동, 모델 폴백, 대화 분기, 프리뷰/토큰 관리, 에이전트 루프 강화. @@ -101,3 +101,6 @@ - AX Agent가 `SettingsService.SettingsChanged`를 직접 구독하도록 바꿔 메인 설정/AX Agent 설정 어느 경로에서 저장하더라도 테마, 모델, 권한, 데이터 활용, composer, 대기열 UI가 즉시 동일 상태를 반영하도록 fan-out을 통합함. - AX Agent 설정 저장 경로에서 표현 수준을 무조건 `rich`로 덮어쓰던 로직을 제거해 `풍부하게 / 적절하게 / 간단하게`가 다른 설정 저장 경로에서도 유지되도록 보정함. - DraftQueue 패널은 `실행 중 / 다음 작업 / 보류 / 완료 / 실패` 개별 섹션 구조로 다시 나눠 현재 실행 흐름과 재시도 대기, 결과 이력을 더 빠르게 파악할 수 있도록 정리함. +- ????: 2026-04-04 23:32 (KST) +- AX Agent ???? ??? ?? tool-result ?? -> microcompact ?? ?? ?? -> ?? ?? ?? 3?? ??? ???, claude-code? staged compact ??? ? ??? ? ?? ??? ?? ????? ???. +- ??? ?? ??, tool_result, ?? ???, ???? ? ???? LLM ?? ?? microcompact_boundary? ?? ??? ?? ?? ?? ?? ?? ?? ??? ??? ???? ?? ??. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d8aa1b3..025dd8c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,5 +1,7 @@ -# AX Copilot — 개발 문서 +# AX Copilot - ?? ?? +- Document update: 2026-04-04 23:32 (KST) - Upgraded ContextCondenser from a two-step path to a three-step path (tool-result truncate -> microcompact -> summarize) so long sessions can strip older execution/tool chatter before invoking the LLM summary stage. +- Document update: 2026-04-04 23:32 (KST) - Added AX-side microcompact boundaries that compress older tool results, execution metadata, and oversized messages into lighter boundary summaries, bringing the compact flow closer to the staged approach used in claude-code. - Document update: 2026-04-04 20:27 (KST) - Added AX Agent theme styles (`claw/codex/slate`) and exposed separate selection inside the in-chat settings. - Document update: 2026-04-04 18:03 (KST) - Added a context usage ring with compact guidance, a direct compact action, and Git branch status badges to the AX Agent composer/footer. - Document update: 2026-04-04 18:03 (KST) - Removed document format/design selectors from the footer and moved their defaults into AX Agent settings, keeping output format on AI automatic selection by default. @@ -4082,4 +4084,4 @@ ow + toggle 시각 언어로 다시 정렬했다. - AX Agent 하단 컨텍스트 카드의 툴팁에 최근 compact 이력을 추가해 마지막 자동/수동 압축 시각, 압축 전후 토큰 수, 절감량을 이후에도 다시 확인할 수 있게 보강했다. - 수동 `/compact`와 전송 전 자동 컨텍스트 압축이 모두 같은 `RecordCompactionStats(...)` 경로를 사용하도록 맞춰, 압축 결과가 일회성 상태 메시지에만 머물지 않고 UI에서 재참조 가능하도록 정리했다. - 이번 라운드부터 `claude-code`의 compact 소스를 기준으로 `session memory compaction → microcompact → context collapse/snip → autocompact → post-compaction 계측` 흐름을 AX 기준과 비교 분석할 준비를 진행했다. -- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0) +- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0) \ No newline at end of file diff --git a/src/AxCopilot/Services/Agent/ContextCondenser.cs b/src/AxCopilot/Services/Agent/ContextCondenser.cs index f24e24b..1bb44d7 100644 --- a/src/AxCopilot/Services/Agent/ContextCondenser.cs +++ b/src/AxCopilot/Services/Agent/ContextCondenser.cs @@ -6,14 +6,17 @@ namespace AxCopilot.Services.Agent; /// 컨텍스트 윈도우 관리: 대화가 길어지면 이전 메시지를 요약하여 압축합니다. /// MemGPT/OpenHands의 LLMSummarizingCondenser 패턴을 경량 구현. /// -/// 2단계 압축 전략: +/// 3단계 압축 전략: /// 1단계 — 도구 결과 자르기: 대용량 tool_result 출력을 핵심만 남기고 축약 (LLM 호출 없음) -/// 2단계 — 이전 대화 요약: 오래된 메시지를 LLM으로 요약하여 교체 (LLM 1회 호출) +/// 2단계 — microcompact: 오래된 실행 로그/도구 묶음을 경계 요약으로 치환 (LLM 호출 없음) +/// 3단계 — 이전 대화 요약: 오래된 메시지를 LLM으로 요약하여 교체 (LLM 1회 호출) /// public static class ContextCondenser { /// 도구 결과 1개당 최대 유지 길이 (자) private const int MaxToolResultChars = 1500; + private const int MicrocompactSingleKeepChars = 480; + private const int MicrocompactGroupMinCount = 2; /// 요약 시 유지할 최근 메시지 수 private const int RecentKeepCount = 6; @@ -36,7 +39,8 @@ public static class ContextCondenser /// /// 메시지 목록의 토큰이 모델 한도에 근접하면 자동 압축합니다. /// 1단계: 도구 결과 축약 (빠르고 LLM 호출 없음) - /// 2단계: 이전 대화 LLM 요약 (토큰이 여전히 높으면) + /// 2단계: microcompact 경량 압축 (토큰이 여전히 높으면) + /// 3단계: 이전 대화 LLM 요약 (토큰이 여전히 높으면) /// public static async Task CondenseIfNeededAsync( List messages, @@ -59,6 +63,7 @@ public static class ContextCondenser var currentTokens = TokenEstimator.EstimateMessages(messages); if (!force && currentTokens < threshold) return false; + var originalTokens = currentTokens; bool didCompress = false; @@ -69,13 +74,19 @@ public static class ContextCondenser currentTokens = TokenEstimator.EstimateMessages(messages); if (!force && currentTokens < threshold) return didCompress; - // ── 2단계: 이전 대화 LLM 요약 ── + // ── 2단계: 오래된 실행/도구 묶음 경량 압축 ── + didCompress |= MicrocompactOlderMessages(messages); + + currentTokens = TokenEstimator.EstimateMessages(messages); + if (!force && currentTokens < threshold) return didCompress; + + // ── 3단계: 이전 대화 LLM 요약 ── didCompress |= await SummarizeOldMessagesAsync(messages, llm, ct); if (didCompress) { var afterTokens = TokenEstimator.EstimateMessages(messages); - LogService.Info($"Context Condenser: {currentTokens} → {afterTokens} 토큰 (절감 {currentTokens - afterTokens})"); + LogService.Info($"Context Condenser: {originalTokens} → {afterTokens} 토큰 (절감 {originalTokens - afterTokens})"); } return didCompress; @@ -149,6 +160,168 @@ public static class ContextCondenser }; } + /// + /// 오래된 실행 로그/도구 결과/비정상적으로 긴 메시지를 먼저 경량 경계 요약으로 묶습니다. + /// claw-code의 microcompact처럼 LLM 호출 전에 토큰을 한 번 더 줄이는 단계입니다. + /// + private static bool MicrocompactOlderMessages(List messages) + { + var systemMsg = messages.FirstOrDefault(m => m.Role == "system"); + var nonSystemMessages = messages.Where(m => m.Role != "system").ToList(); + if (nonSystemMessages.Count <= RecentKeepCount + 2) return false; + + var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count); + var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList(); + var oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList(); + + var rewritten = new List(oldMessages.Count); + bool changed = false; + + for (int i = 0; i < oldMessages.Count;) + { + if (IsMicrocompactCandidate(oldMessages[i])) + { + var group = new List(); + int j = i; + while (j < oldMessages.Count && IsMicrocompactCandidate(oldMessages[j])) + { + group.Add(oldMessages[j]); + j++; + } + + var totalChars = group.Sum(m => m.Content?.Length ?? 0); + if (group.Count >= MicrocompactGroupMinCount || totalChars > MaxToolResultChars * 2) + { + rewritten.Add(BuildMicrocompactBoundary(group)); + changed = true; + } + else + { + var singleChanged = TryMicrocompactSingle(group[0], out var compacted); + rewritten.Add(singleChanged ? compacted : group[0]); + changed |= singleChanged; + } + + i = j; + continue; + } + + if (TryMicrocompactSingle(oldMessages[i], out var singleCompacted)) + { + rewritten.Add(singleCompacted); + changed = true; + } + else + { + rewritten.Add(oldMessages[i]); + } + + i++; + } + + if (!changed) return false; + + messages.Clear(); + if (systemMsg != null) messages.Add(systemMsg); + messages.AddRange(rewritten); + messages.AddRange(recentMessages); + return true; + } + + private static bool IsMicrocompactCandidate(ChatMessage message) + { + var content = message.Content ?? ""; + if (string.IsNullOrWhiteSpace(content)) return false; + + return message.MetaKind != null + || content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal) + || content.StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal) + || (message.Role != "system" && content.Length > MaxToolResultChars); + } + + private static bool TryMicrocompactSingle(ChatMessage source, out ChatMessage compacted) + { + compacted = source; + var content = source.Content ?? ""; + if (string.IsNullOrWhiteSpace(content)) return false; + if (source.Role == "system") return false; + + if (content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal)) + { + compacted = CloneWithContent( + source, + $"[이전 도구 결과 축약]\n- 상세 출력은 압축되었습니다.\n- 원본 길이: {content.Length:N0}자"); + return true; + } + + if (content.StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal)) + { + compacted = CloneWithContent( + source, + $"[이전 도구 호출 묶음 축약]\n- 도구 호출 구조는 유지하지 않고 요약만 남겼습니다.\n- 원본 길이: {content.Length:N0}자"); + return true; + } + + if (source.MetaKind != null) + { + compacted = CloneWithContent( + source, + $"[이전 실행 메타 축약]\n- 종류: {source.MetaKind}\n- 자세한 실행 이벤트는 압축되었습니다."); + return true; + } + + if (content.Length <= MaxToolResultChars) return false; + + var head = content[..Math.Min(MicrocompactSingleKeepChars, content.Length)]; + compacted = CloneWithContent( + source, + head + $"\n\n...[이전 메시지 microcompact 적용 — 원본 {content.Length:N0}자]"); + return true; + } + + private static ChatMessage BuildMicrocompactBoundary(List group) + { + int toolResults = group.Count(m => (m.Content ?? "").StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal)); + int toolCalls = group.Count(m => (m.Content ?? "").StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal)); + int metaEvents = group.Count(m => m.MetaKind != null); + int longTexts = group.Count(m => + !(m.Content ?? "").StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal) && + !(m.Content ?? "").StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal) && + m.MetaKind == null); + + var attachedFiles = group + .SelectMany(m => m.AttachedFiles ?? Enumerable.Empty()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(path => + { + try { return System.IO.Path.GetFileName(path); } + catch { return path; } + }) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Take(4) + .ToList(); + + var lines = new List + { + $"[이전 실행 묶음 압축 — {group.Count}개 메시지]" + }; + + if (toolResults > 0) lines.Add($"- 도구 결과 {toolResults}건 정리"); + if (toolCalls > 0) lines.Add($"- 도구 호출 {toolCalls}건 정리"); + if (metaEvents > 0) lines.Add($"- 실행 메타/로그 {metaEvents}건 정리"); + if (longTexts > 0) lines.Add($"- 긴 설명/출력 {longTexts}건 축약"); + if (attachedFiles.Count > 0) lines.Add($"- 관련 파일: {string.Join(", ", attachedFiles)}"); + + return new ChatMessage + { + Role = "assistant", + Content = string.Join("\n", lines), + Timestamp = group.Last().Timestamp, + MetaKind = "microcompact_boundary", + MetaRunId = group.Last().MetaRunId, + }; + } + /// tool_result JSON 내의 output 값을 축약합니다. private static string TruncateToolResultJson(string json) {