diff --git a/README.md b/README.md index 0a61c71..c2c33e2 100644 --- a/README.md +++ b/README.md @@ -1402,3 +1402,6 @@ MIT License - 업데이트: 2026-04-07 00:22 (KST) - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs)에 `paths:` frontmatter 지원을 추가해 `.ax/rules/*.md` 같은 계층형 메모리 문서가 특정 작업 폴더 범위에서만 적용되도록 했습니다. - 이제 메모리 문서 상단에 `---`, `paths:`, `- src/**`, `---` 형태를 쓰면 현재 작업 폴더가 프로젝트 루트 기준으로 그 패턴에 맞을 때만 로드됩니다. AX 메모리 규칙을 `claw-code`의 경로별 rule 파일처럼 더 세밀하게 제어할 수 있습니다. +- 업데이트: 2026-04-07 00:31 (KST) + - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs)에 `description:` frontmatter 메타를 추가해 계층형 메모리 규칙 파일이 “무엇을 위한 규칙인지” 설명을 가질 수 있게 했습니다. + - [MemoryTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/MemoryTool.cs) 는 `show_scope` 액션을 새로 지원합니다. 이제 `/memory` 계열 명령으로 `managed / user / project / local` 메모리 파일의 실제 내용을 직접 확인할 수 있고, `list/search` 결과에도 `description`과 `paths` 범위가 함께 표시됩니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 0d1492b..af45007 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5175,3 +5175,13 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) - 간단한 glob 매처를 추가해 `*`, `**`, `?` 패턴을 지원한다. - 현재는 `.ax/rules/*.md` 같은 프로젝트 규칙 파일을 `claw-code`의 경로 범위 rules와 비슷하게 운영할 수 있는 수준까지 올라온 상태다. + +## 2026-04-07 00:31 (KST) + +- [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) + - 계층형 메모리 frontmatter에 `description:` 메타를 추가했다. + - `paths:` 범위 판정 뒤 실제 주입 문서에는 설명과 경로 범위를 함께 보관해, `/memory list`와 `/memory search`에서 규칙 파일의 의도를 더 잘 읽을 수 있게 했다. + - scope별 원본 메모리 파일을 직접 읽을 수 있는 `ReadInstructionFile(...)`도 추가했다. +- [MemoryTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/MemoryTool.cs) + - `show_scope` 액션을 추가해 `managed / user / project / local` 메모리 파일의 실제 경로와 본문을 바로 확인할 수 있게 했다. + - `list`, `search` 출력에는 계층형 메모리 문서의 `description`과 `paths` 메타를 함께 노출해, `claw-code`의 rule 파일처럼 어떤 규칙이 어느 범위에 적용되는지 빠르게 읽을 수 있도록 정리했다. diff --git a/src/AxCopilot/Services/Agent/MemoryTool.cs b/src/AxCopilot/Services/Agent/MemoryTool.cs index af4c6ce..66d8d1b 100644 --- a/src/AxCopilot/Services/Agent/MemoryTool.cs +++ b/src/AxCopilot/Services/Agent/MemoryTool.cs @@ -16,6 +16,7 @@ public class MemoryTool : IAgentTool "대화 간 지속되는 메모리로, 새 대화에서도 이전에 학습한 내용을 활용할 수 있습니다.\n" + "- action=\"save\": 학습 메모리 저장 (type, content 필수)\n" + "- action=\"save_scope\": 계층형 메모리 파일에 저장 (scope, content 필수)\n" + + "- action=\"show_scope\": 계층형 메모리 파일 본문 조회 (scope 필수)\n" + "- action=\"search\": 관련 메모리 검색 (query 필수)\n" + "- action=\"list\": 현재 메모리 전체 목록 + 계층형 메모리 파일 목록\n" + "- action=\"delete\": 학습 메모리 삭제 (id 필수)\n" + @@ -27,7 +28,7 @@ public class MemoryTool : IAgentTool { Properties = new() { - ["action"] = new() { Type = "string", Description = "save | save_scope | search | list | delete | delete_scope" }, + ["action"] = new() { Type = "string", Description = "save | save_scope | show_scope | search | list | delete | delete_scope" }, ["type"] = new() { Type = "string", Description = "메모리 유형: rule | preference | fact | correction. save 시 필수." }, ["scope"] = new() { Type = "string", Description = "계층형 메모리 대상: managed | user | project | local. save_scope/delete_scope 시 필수." }, ["content"] = new() { Type = "string", Description = "저장할 내용. save 시 필수." }, @@ -58,11 +59,12 @@ public class MemoryTool : IAgentTool { "save" => ExecuteSave(args, memoryService, context), "save_scope" => ExecuteSaveScope(args, memoryService, context), + "show_scope" => ExecuteShowScope(args, memoryService, context), "search" => ExecuteSearch(args, memoryService), "list" => ExecuteList(memoryService), "delete" => ExecuteDelete(args, memoryService), "delete_scope" => ExecuteDeleteScope(args, memoryService, context), - _ => ToolResult.Fail($"알 수 없는 액션: {action}. save | save_scope | search | list | delete | delete_scope 중 선택하세요."), + _ => ToolResult.Fail($"알 수 없는 액션: {action}. save | save_scope | show_scope | search | list | delete | delete_scope 중 선택하세요."), }); } @@ -104,7 +106,11 @@ public class MemoryTool : IAgentTool { sb.AppendLine($"계층형 메모리 {docs.Count}개:"); foreach (var doc in docs) - sb.AppendLine($" [{doc.Label}] {doc.Path}"); + { + var suffix = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" — {doc.Description}"; + var scopeHint = doc.Paths.Count > 0 ? $" (paths: {string.Join(", ", doc.Paths)})" : ""; + sb.AppendLine($" [{doc.Label}] {doc.Path}{suffix}{scopeHint}"); + } sb.AppendLine(); } @@ -129,7 +135,11 @@ public class MemoryTool : IAgentTool { sb.AppendLine($"계층형 메모리 파일 {docs.Count}개:"); foreach (var doc in docs) - sb.AppendLine($" • [{doc.Label}] {doc.Path}"); + { + var suffix = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" — {doc.Description}"; + var scopeHint = doc.Paths.Count > 0 ? $" (paths: {string.Join(", ", doc.Paths)})" : ""; + sb.AppendLine($" • [{doc.Label}] {doc.Path}{suffix}{scopeHint}"); + } sb.AppendLine(); } @@ -186,4 +196,21 @@ public class MemoryTool : IAgentTool ? ToolResult.Ok($"{result.Message}\n경로: {result.Path}") : ToolResult.Fail(result.Message); } + + private static ToolResult ExecuteShowScope(JsonElement args, AgentMemoryService svc, AgentContext context) + { + var scope = args.TryGetProperty("scope", out var s) ? s.GetString() ?? "" : ""; + if (string.IsNullOrWhiteSpace(scope)) + return ToolResult.Fail("scope가 필요합니다. managed | user | project | local 중 선택하세요."); + + var path = svc.GetWritableInstructionPath(scope, context.WorkFolder); + if (string.IsNullOrWhiteSpace(path)) + return ToolResult.Fail("해당 scope의 메모리 파일 경로를 결정할 수 없습니다."); + + var content = svc.ReadInstructionFile(scope, context.WorkFolder); + if (content == null) + return ToolResult.Ok($"메모리 파일이 아직 없습니다.\n경로: {path}"); + + return ToolResult.Ok($"메모리 파일 경로: {path}\n\n{content}"); + } } diff --git a/src/AxCopilot/Services/AgentMemoryService.cs b/src/AxCopilot/Services/AgentMemoryService.cs index 8bab2e4..dcb8c2f 100644 --- a/src/AxCopilot/Services/AgentMemoryService.cs +++ b/src/AxCopilot/Services/AgentMemoryService.cs @@ -415,7 +415,8 @@ public class AgentMemoryService Label = label, Path = fullPath, Content = frontMatter.Content.Trim(), - Paths = frontMatter.Paths + Paths = frontMatter.Paths, + Description = frontMatter.Description }); } catch (Exception ex) @@ -618,11 +619,31 @@ public class AgentMemoryService } } - private static (string Content, List Paths) ParseFrontMatter(string content) + public string? ReadInstructionFile(string scope, string? workFolder) + { + lock (_lock) + { + var path = GetWritableInstructionPath(scope, workFolder); + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + return null; + + try + { + return File.ReadAllText(path); + } + catch (Exception ex) + { + LogService.Warn($"메모리 파일 읽기 실패 ({path}): {ex.Message}"); + return null; + } + } + } + + private static (string Content, List Paths, string Description) ParseFrontMatter(string content) { var lines = content.Replace("\r\n", "\n").Split('\n').ToList(); if (lines.Count < 3 || !string.Equals(lines[0].Trim(), "---", StringComparison.Ordinal)) - return (content, new List()); + return (content, new List(), ""); var endIndex = -1; for (var i = 1; i < lines.Count; i++) @@ -635,13 +656,20 @@ public class AgentMemoryService } if (endIndex < 1) - return (content, new List()); + return (content, new List(), ""); var paths = new List(); + var description = ""; var inPaths = false; for (var i = 1; i < endIndex; i++) { var line = lines[i].Trim(); + if (line.StartsWith("description:", StringComparison.OrdinalIgnoreCase)) + { + description = line["description:".Length..].Trim().Trim('"'); + inPaths = false; + continue; + } if (line.StartsWith("paths:", StringComparison.OrdinalIgnoreCase)) { inPaths = true; @@ -664,7 +692,7 @@ public class AgentMemoryService } var stripped = string.Join("\n", lines.Skip(endIndex + 1)).Trim(); - return (stripped, paths); + return (stripped, paths, description); } private static bool ShouldApplyToCurrentWorkFolder(IReadOnlyList patterns, string? projectRoot, string? currentWorkFolder) @@ -773,4 +801,7 @@ public class MemoryInstructionDocument [JsonPropertyName("paths")] public List Paths { get; set; } = new(); + + [JsonPropertyName("description")] + public string Description { get; set; } = ""; }