using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using AxCopilot.Models; namespace AxCopilot.Services.Agent; public static class ContextCondenser { private const int MaxToolResultChars = 1500; private const int RecentKeepCount = 6; private static int GetModelInputLimit(string service, string model) { string text = (service + ":" + model).ToLowerInvariant(); string text2 = text; if (1 == 0) { } int result = ((!text.Contains("claude")) ? ((!text.Contains("gemini-2.5")) ? ((!text.Contains("gemini-2.0")) ? ((!text.Contains("gemini")) ? ((!text.Contains("gpt-4")) ? 16000 : 120000) : 900000) : 900000) : 900000) : 180000); if (1 == 0) { } return result; } public static async Task CondenseIfNeededAsync(List messages, LlmService llm, int maxOutputTokens, CancellationToken ct = default(CancellationToken)) { if (messages.Count < 6) { return false; } (string service, string model) settings = llm.GetCurrentModelInfo(); int inputLimit = GetModelInputLimit(settings.service, settings.model); int threshold = (int)((double)inputLimit * 0.65); int currentTokens = TokenEstimator.EstimateMessages(messages); if (currentTokens < threshold) { return false; } bool didCompress = false; didCompress |= TruncateToolResults(messages); currentTokens = TokenEstimator.EstimateMessages(messages); if (currentTokens < threshold) { return didCompress; } bool flag = didCompress; didCompress = flag | await SummarizeOldMessagesAsync(messages, llm, ct); if (didCompress) { int afterTokens = TokenEstimator.EstimateMessages(messages); LogService.Info($"Context Condenser: {currentTokens} → {afterTokens} 토큰 (절감 {currentTokens - afterTokens})"); } return didCompress; } private static bool TruncateToolResults(List messages) { bool result = false; int num = Math.Max(0, messages.Count - 6); for (int i = 0; i < num; i++) { ChatMessage chatMessage = messages[i]; if (chatMessage.Content == null) { continue; } if (chatMessage.Content.StartsWith("{\"type\":\"tool_result\"") && chatMessage.Content.Length > 1500) { messages[i] = new ChatMessage { Role = chatMessage.Role, Content = TruncateToolResultJson(chatMessage.Content), Timestamp = chatMessage.Timestamp }; result = true; } else if (chatMessage.Role == "assistant" && chatMessage.Content.Length > 3000 && chatMessage.Content.StartsWith("{\"_tool_use_blocks\"")) { if (chatMessage.Content.Length > 4500) { messages[i] = new ChatMessage { Role = chatMessage.Role, Content = chatMessage.Content.Substring(0, 3000) + "...[축약됨]\"]}", Timestamp = chatMessage.Timestamp }; result = true; } } else if (chatMessage.Content.Length > 4500 && chatMessage.Role != "system") { messages[i] = new ChatMessage { Role = chatMessage.Role, Content = chatMessage.Content.Substring(0, 1500) + "\n\n...[이전 내용 축약됨 — 원본 " + $"{chatMessage.Content.Length:N0}자 중 {1500:N0}자 유지]", Timestamp = chatMessage.Timestamp }; result = true; } } return result; } private static string TruncateToolResultJson(string json) { int num = json.IndexOf("\"output\":\"", StringComparison.Ordinal); if (num < 0) { return json.Substring(0, Math.Min(json.Length, 1500)) + "...[축약됨]}"; } int num2 = num + "\"output\":\"".Length; int num3 = num2; while (num3 < json.Length) { if (json[num3] == '\\') { num3 += 2; continue; } if (json[num3] == '"') { break; } num3++; } int num4 = num3 - num2; if (num4 <= 1500) { return json; } int num5 = 750; string text = json.Substring(0, num2); int num6 = num2; string text2 = json.Substring(num6, num3 - num6); num6 = num3; string text3 = json.Substring(num6, json.Length - num6); string[] obj = new string[9] { text, text2.Substring(0, num5), "\\n...[축약됨: ", $"{num4:N0}", "자 중 ", $"{1500:N0}", "자 유지]\\n", null, null }; num6 = num5; int length = text2.Length; int num7 = length - num6; obj[7] = text2.Substring(num7, length - num7); obj[8] = text3; return string.Concat(obj); } private static async Task SummarizeOldMessagesAsync(List messages, LlmService llm, CancellationToken ct) { ChatMessage systemMsg = messages.FirstOrDefault((ChatMessage chatMessage) => chatMessage.Role == "system"); if (systemMsg == null) { } List nonSystemMessages = messages.Where((ChatMessage chatMessage) => chatMessage.Role != "system").ToList(); int keepCount = Math.Min(6, nonSystemMessages.Count); List recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList(); List oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList(); if (oldMessages.Count < 3) { return false; } StringBuilder sb = new StringBuilder(); sb.AppendLine("다음 대화 기록을 간결하게 요약하세요."); sb.AppendLine("반드시 유지할 정보: 사용자 요청, 핵심 결정 사항, 생성/수정된 파일 경로, 작업 진행 상황, 중요한 결과."); sb.AppendLine("제거할 정보: 도구 실행의 상세 출력, 반복되는 내용, 중간 사고 과정."); sb.AppendLine("요약은 대화와 동일한 언어로 작성하세요. 글머리 기호(-)로 핵심 사항만 나열하세요.\n---"); foreach (ChatMessage m in oldMessages) { string content = m.Content ?? ""; if (content.StartsWith("{\"_tool_use_blocks\"")) { content = "[도구 호출]"; } else if (content.StartsWith("{\"type\":\"tool_result\"")) { content = "[도구 결과]"; } else if (content.Length > 300) { content = content.Substring(0, 300) + "..."; } StringBuilder stringBuilder = sb; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(4, 2, stringBuilder); handler.AppendLiteral("["); handler.AppendFormatted(m.Role); handler.AppendLiteral("]: "); handler.AppendFormatted(content); stringBuilder.AppendLine(ref handler); } try { List summaryMessages = new List { new ChatMessage { Role = "user", Content = sb.ToString() } }; string summary = await llm.SendAsync(summaryMessages, ct); if (string.IsNullOrEmpty(summary)) { return false; } messages.Clear(); if (systemMsg != null) { messages.Add(systemMsg); } messages.Add(new ChatMessage { Role = "user", Content = $"[이전 대화 요약 — {oldMessages.Count}개 메시지 압축]\n{summary}", Timestamp = DateTime.Now }); messages.Add(new ChatMessage { Role = "assistant", Content = "이전 대화 내용을 확인했습니다. 이어서 작업하겠습니다.", Timestamp = DateTime.Now }); messages.AddRange(recentMessages); return true; } catch (Exception ex) { LogService.Warn("Context Condenser 요약 실패: " + ex.Message); return false; } } }