From 13cd1e54edc99762e7399a4bd67aec3e004f16bb Mon Sep 17 00:00:00 2001 From: lacvet Date: Mon, 6 Apr 2026 23:40:35 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=201=EC=B0=A8=20=EA=B0=95=ED=99=94:=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=ED=98=95=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EB=A1=9C=EB=94=A9=EA=B3=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=A3=BC=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AgentMemoryService에 관리형/사용자/프로젝트/로컬 메모리 문서 탐색을 추가해 AXMEMORY.md, AXMEMORY.local.md, .ax/rules/*.md 계층을 로드하도록 확장함 - ChatWindow 시스템 프롬프트 메모리 섹션을 계층형 메모리와 기존 학습 메모리를 함께 조립하는 구조로 재편함 - 작업 폴더 메모리 로드 전에 Count를 먼저 검사하던 경로를 제거해 다른 폴더 메모리 누락 가능성을 줄임 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0) --- README.md | 3 + docs/DEVELOPMENT.md | 11 ++ src/AxCopilot/Services/AgentMemoryService.cs | 116 +++++++++++++++++++ src/AxCopilot/Views/ChatWindow.xaml.cs | 56 ++++++--- 4 files changed, 168 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c83bac7..e3d2603 100644 --- a/README.md +++ b/README.md @@ -1387,3 +1387,6 @@ MIT License - 런처 검색 반응성을 높이기 위해 [FuzzyEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Core/FuzzyEngine.cs)에 인덱스 버전 기준 쿼리 캐시를 추가했습니다. 색인이 같은 상태에서 반복 입력되는 쿼리는 결과를 다시 전부 계산하지 않고 즉시 재사용합니다. - 앱 시작 직후 캐시된 인덱스가 없을 때는 런처 watcher를 먼저 모두 켜지 않도록 [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs)를 조정했습니다. 불필요한 감시기 오버헤드를 줄이고, 실제 첫 색인 완료 뒤에 watcher가 붙도록 정리했습니다. - AX Agent는 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 최소화/백그라운드 상태일 때 task summary, 입력 보조 UI, 에이전트 상태 반영을 즉시 다시 그리지 않고 대기시켰다가 다시 활성화될 때 한 번에 flush 하도록 바꿨습니다. +- 업데이트: 2026-04-06 23:49 (KST) + - AX Agent 메모리 구조 강화를 시작했습니다. [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs)에 `관리형 / 사용자 / 프로젝트 / 로컬` 계층형 메모리 문서 로더를 추가해 `AXMEMORY.md`, `AXMEMORY.local.md`, `.ax/rules/*.md` 계열 파일을 현재 작업 폴더까지 발견하고 로드합니다. + - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 시스템 프롬프트 메모리 섹션도 계층형 메모리 + 기존 학습 메모리를 함께 조립하도록 바꿨습니다. 이제 AX는 `claw-code`처럼 지속 메모리를 단순 전역/폴더 저장이 아니라 계층형 지시문 + 학습형 메모리의 조합으로 주입합니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3245165..5bab6c7 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5123,3 +5123,14 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) - 최소화/비활성/백그라운드 상태를 `IsBackgroundUiThrottleActive()`로 판단해 task summary, 입력 보조 UI, agent UI event flush를 바로 수행하지 않고 pending 상태로 넘긴다. - 창이 다시 활성화되면 `FlushDeferredUiRefreshIfNeeded()`에서 누적된 갱신을 한 번에 반영해, 백그라운드 상태의 잦은 UI 타이머 churn을 줄였다. + +## 2026-04-06 23:49 (KST) + +- [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) + - AX 메모리 구조 강화 1차로 `관리형 / 사용자 / 프로젝트 / 로컬` 계층형 메모리 문서를 지원하도록 확장했다. + - 탐색 대상은 `%ProgramData%\\AxCopilot\\memory\\AXMEMORY.md`, `%APPDATA%\\AxCopilot\\memory\\AXMEMORY.md`, 작업 디렉토리까지의 `AXMEMORY.md`, `.ax\\AXMEMORY.md`, `.ax\\rules\\*.md`, `AXMEMORY.local.md` 이다. + - 기존 암호화 저장형 학습 메모리(`_global.dat`, 작업 폴더 해시 `.dat`)는 그대로 유지하고, 새 계층형 문서는 `MemoryInstructionDocument` 컬렉션으로 별도 관리한다. +- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) + - 시스템 프롬프트의 메모리 섹션을 `메모리 계층` + `학습 메모리` 2단 구조로 재편했다. + - 이전에는 학습 메모리만 단순 나열했지만, 이제 `claw-code`처럼 계층형 메모리 파일을 우선순위 순서로 조립하고 그 아래에 학습형 메모리를 추가한다. + - 로드 전에 `Count`를 먼저 확인해 다른 작업 폴더 메모리가 누락될 수 있던 경로도 함께 바로잡았다. diff --git a/src/AxCopilot/Services/AgentMemoryService.cs b/src/AxCopilot/Services/AgentMemoryService.cs index 502743c..00a0ea4 100644 --- a/src/AxCopilot/Services/AgentMemoryService.cs +++ b/src/AxCopilot/Services/AgentMemoryService.cs @@ -16,6 +16,11 @@ public class AgentMemoryService private static readonly string MemoryDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "memory"); + private static readonly string ManagedMemoryDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "AxCopilot", "memory"); + private const string MemoryFileName = "AXMEMORY.md"; + private const string LocalMemoryFileName = "AXMEMORY.local.md"; private static readonly JsonSerializerOptions JsonOptions = new() { @@ -24,6 +29,7 @@ public class AgentMemoryService }; private readonly List _entries = new(); + private readonly List _instructionDocuments = new(); private readonly object _lock = new(); private string? _currentWorkFolder; @@ -33,12 +39,19 @@ public class AgentMemoryService /// 모든 메모리 항목 (읽기 전용). public IReadOnlyList All { get { lock (_lock) return _entries.ToList(); } } + /// 현재 로드된 계층형 메모리 문서 (읽기 전용). + public IReadOnlyList InstructionDocuments + { + get { lock (_lock) return _instructionDocuments.ToList(); } + } + /// 작업 폴더별 메모리 + 전역 메모리를 로드합니다. public void Load(string? workFolder) { lock (_lock) { _entries.Clear(); + _instructionDocuments.Clear(); _currentWorkFolder = workFolder; // 전역 메모리 @@ -51,6 +64,8 @@ public class AgentMemoryService var folderPath = GetFilePath(workFolder); LoadFromFile(folderPath); } + + LoadInstructionDocuments(workFolder); } } @@ -255,6 +270,91 @@ public class AgentMemoryService return union > 0 ? (double)intersection / union : 0; } + private void LoadInstructionDocuments(string? workFolder) + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + AddInstructionFileIfExists(Path.Combine(ManagedMemoryDir, MemoryFileName), "managed", "관리형 메모리", seen); + AddInstructionRuleFilesIfExists(Path.Combine(ManagedMemoryDir, "rules"), "managed", "관리형 메모리", seen); + + AddInstructionFileIfExists(Path.Combine(MemoryDir, MemoryFileName), "user", "사용자 메모리", seen); + AddInstructionRuleFilesIfExists(Path.Combine(MemoryDir, "rules"), "user", "사용자 메모리", seen); + + if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder)) + return; + + foreach (var directory in EnumerateDirectoryChain(workFolder)) + { + AddInstructionFileIfExists(Path.Combine(directory, MemoryFileName), "project", "프로젝트 메모리", seen); + AddInstructionFileIfExists(Path.Combine(directory, ".ax", MemoryFileName), "project", "프로젝트 메모리", seen); + AddInstructionRuleFilesIfExists(Path.Combine(directory, ".ax", "rules"), "project", "프로젝트 메모리", seen); + AddInstructionFileIfExists(Path.Combine(directory, LocalMemoryFileName), "local", "로컬 메모리", seen); + } + } + + private void AddInstructionRuleFilesIfExists(string rulesDirectory, string layer, string label, HashSet seen) + { + try + { + if (!Directory.Exists(rulesDirectory)) + return; + + foreach (var file in Directory.GetFiles(rulesDirectory, "*.md").OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + AddInstructionFileIfExists(file, layer, label, seen); + } + catch (Exception ex) + { + LogService.Warn($"메모리 rules 로드 실패 ({rulesDirectory}): {ex.Message}"); + } + } + + private void AddInstructionFileIfExists(string path, string layer, string label, HashSet seen) + { + try + { + if (!File.Exists(path)) + return; + + var fullPath = Path.GetFullPath(path); + if (!seen.Add(fullPath)) + return; + + var content = File.ReadAllText(fullPath); + if (string.IsNullOrWhiteSpace(content)) + return; + + _instructionDocuments.Add(new MemoryInstructionDocument + { + Layer = layer, + Label = label, + Path = fullPath, + Content = content.Trim() + }); + } + catch (Exception ex) + { + LogService.Warn($"메모리 문서 로드 실패 ({path}): {ex.Message}"); + } + } + + private static IEnumerable EnumerateDirectoryChain(string workFolder) + { + var current = Path.GetFullPath(workFolder); + var stack = new Stack(); + + while (!string.IsNullOrWhiteSpace(current)) + { + stack.Push(current); + var parent = Directory.GetParent(current)?.FullName; + if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, current, StringComparison.OrdinalIgnoreCase)) + break; + current = parent; + } + + while (stack.Count > 0) + yield return stack.Pop(); + } + /// 텍스트를 토큰으로 분리합니다. private static HashSet Tokenize(string text) { @@ -307,3 +407,19 @@ public class MemoryEntry [JsonPropertyName("workFolder")] public string? WorkFolder { get; set; } } + +/// CLAUDE.md 스타일의 계층형 메모리 문서. +public class MemoryInstructionDocument +{ + [JsonPropertyName("layer")] + public string Layer { get; set; } = "project"; + + [JsonPropertyName("label")] + public string Label { get; set; } = "프로젝트 메모리"; + + [JsonPropertyName("path")] + public string Path { get; set; } = ""; + + [JsonPropertyName("content")] + public string Content { get; set; } = ""; +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index e4687fa..f7b6c07 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -6101,36 +6101,56 @@ public partial class ChatWindow : Window var app = System.Windows.Application.Current as App; var memService = app?.MemoryService; - if (memService == null || memService.Count == 0) return ""; + if (memService == null) return ""; // 메모리를 로드 (작업 폴더 변경 시 재로드) memService.Load(workFolder ?? ""); var all = memService.All; - if (all.Count == 0) return ""; + var layeredDocs = memService.InstructionDocuments; + if (all.Count == 0 && layeredDocs.Count == 0) return ""; var sb = new System.Text.StringBuilder(); - sb.AppendLine("\n## 프로젝트 메모리 (이전 대화에서 학습한 내용)"); - sb.AppendLine("아래는 이전 대화에서 학습한 규칙과 선호도입니다. 작업 시 참고하세요."); - sb.AppendLine("새로운 규칙이나 선호도를 발견하면 memory 도구의 save 액션으로 저장하세요."); - sb.AppendLine("사용자가 이전 학습 내용과 다른 지시를 하면 memory 도구의 delete 후 새로 save 하세요.\n"); + sb.AppendLine("\n## 메모리 계층"); + sb.AppendLine("다음 메모리는 claude-code와 비슷하게 관리형 → 사용자 → 프로젝트 → 로컬 순서로 조립됩니다."); + sb.AppendLine("현재 작업 디렉토리에 가까운 메모리가 더 높은 우선순위를 가집니다.\n"); - foreach (var group in all.GroupBy(e => e.Type)) + const int maxLayeredDocs = 8; + const int maxDocChars = 1800; + foreach (var doc in layeredDocs.Take(maxLayeredDocs)) { - var label = group.Key switch - { - "rule" => "프로젝트 규칙", - "preference" => "사용자 선호", - "fact" => "프로젝트 사실", - "correction" => "이전 교정", - _ => group.Key, - }; - sb.AppendLine($"[{label}]"); - foreach (var e in group.OrderByDescending(e => e.UseCount).Take(15)) - sb.AppendLine($"- {e.Content}"); + sb.AppendLine($"[{doc.Label}] {doc.Path}"); + var text = doc.Content; + if (text.Length > maxDocChars) + text = text[..maxDocChars] + "\n...(생략)"; + sb.AppendLine(text); sb.AppendLine(); } + if (all.Count > 0) + { + sb.AppendLine("## 학습 메모리 (이전 대화에서 학습한 내용)"); + sb.AppendLine("아래는 이전 대화에서 학습한 규칙과 선호도입니다. 작업 시 참고하세요."); + sb.AppendLine("새로운 규칙이나 선호도를 발견하면 memory 도구의 save 액션으로 저장하세요."); + sb.AppendLine("사용자가 이전 학습 내용과 다른 지시를 하면 memory 도구의 delete 후 새로 save 하세요.\n"); + + foreach (var group in all.GroupBy(e => e.Type)) + { + var label = group.Key switch + { + "rule" => "프로젝트 규칙", + "preference" => "사용자 선호", + "fact" => "프로젝트 사실", + "correction" => "이전 교정", + _ => group.Key, + }; + sb.AppendLine($"[{label}]"); + foreach (var e in group.OrderByDescending(e => e.UseCount).Take(15)) + sb.AppendLine($"- {e.Content}"); + sb.AppendLine(); + } + } + return sb.ToString(); }