AX Agent 메모리 구조 1차 강화: 계층형 메모리 문서 로딩과 프롬프트 주입 추가
Some checks failed
Release Gate / gate (push) Has been cancelled

- 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)
This commit is contained in:
2026-04-06 23:40:35 +09:00
parent c75790f8c2
commit 13cd1e54ed
4 changed files with 168 additions and 18 deletions

View File

@@ -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`처럼 지속 메모리를 단순 전역/폴더 저장이 아니라 계층형 지시문 + 학습형 메모리의 조합으로 주입합니다.

View File

@@ -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`를 먼저 확인해 다른 작업 폴더 메모리가 누락될 수 있던 경로도 함께 바로잡았다.

View File

@@ -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<MemoryEntry> _entries = new();
private readonly List<MemoryInstructionDocument> _instructionDocuments = new();
private readonly object _lock = new();
private string? _currentWorkFolder;
@@ -33,12 +39,19 @@ public class AgentMemoryService
/// <summary>모든 메모리 항목 (읽기 전용).</summary>
public IReadOnlyList<MemoryEntry> All { get { lock (_lock) return _entries.ToList(); } }
/// <summary>현재 로드된 계층형 메모리 문서 (읽기 전용).</summary>
public IReadOnlyList<MemoryInstructionDocument> InstructionDocuments
{
get { lock (_lock) return _instructionDocuments.ToList(); }
}
/// <summary>작업 폴더별 메모리 + 전역 메모리를 로드합니다.</summary>
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<string>(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<string> 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<string> 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<string> EnumerateDirectoryChain(string workFolder)
{
var current = Path.GetFullPath(workFolder);
var stack = new Stack<string>();
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();
}
/// <summary>텍스트를 토큰으로 분리합니다.</summary>
private static HashSet<string> Tokenize(string text)
{
@@ -307,3 +407,19 @@ public class MemoryEntry
[JsonPropertyName("workFolder")]
public string? WorkFolder { get; set; }
}
/// <summary>CLAUDE.md 스타일의 계층형 메모리 문서.</summary>
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; } = "";
}

View File

@@ -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();
}