diff --git a/docs/AGENT_ROADMAP.md b/docs/AGENT_ROADMAP.md index 323783d..c7160c6 100644 --- a/docs/AGENT_ROADMAP.md +++ b/docs/AGENT_ROADMAP.md @@ -241,13 +241,16 @@ | 17-D2 | **경로 기반 스킬 활성화** | paths: 프론트매터 — 해당 파일 작업 시 스킬 자동 컨텍스트 주입 | 높음 | | 17-D3 | **스킬 범위 훅·모델 오버라이드** | hooks: (스킬 범위 훅), model: (스킬별 모델), user-invocable:false (AI 전용) | 중간 | -### Group E — 메모리/컨텍스트 고도화 (CC 메모리 문서 기반) +### Group E — 메모리/컨텍스트 고도화 (CC 메모리 문서 기반) ✅ 완료 -| # | 기능 | 설명 | 우선순위 | -|---|------|------|----------| -| 17-E1 | **@include 지시어** | AX.md / .ax/rules에서 @파일경로로 외부 파일 포함. 최대 5단계 | 높음 | -| 17-E2 | **경로 기반 규칙 주입** | .ax/rules/*.md의 paths: — 해당 파일 작업 시만 규칙 주입 | 높음 | -| 17-E3 | **컨텍스트 컴팩션 + 파일 되감기** | /compact 명령, PreCompact/PostCompact 훅, 파일 변경 되감기 | 중간 | +| # | 기능 | 설명 | 우선순위 | 구현 | +|---|------|------|----------|------| +| 17-E1 | **@include 지시어** | AX.md / .ax/rules에서 @파일경로로 외부 파일 포함. 최대 5단계 | 높음 | AxMdIncludeResolver.ResolveAsync() — AgentLoopService.Memory.cs에서 세션 시작 시 호출 | +| 17-E2 | **경로 기반 규칙 주입** | .ax/rules/*.md의 paths: — 해당 파일 작업 시만 규칙 주입 | 높음 | PathScopedRuleInjector — 파일 도구 결과 후 InjectPathScopedRulesAsync() 호출 | +| 17-E3 | **컨텍스트 컴팩션 + 파일 되감기** | /compact 명령, PreCompact/PostCompact 훅, 파일 변경 되감기 | 중간 | — (차기 Phase) | + +새 파일: `AgentLoopService.Memory.cs` (105줄) — `InjectHierarchicalMemoryAsync()` + `InjectPathScopedRulesAsync()` +설정 추가: `EnableMemorySystem` (기본 true) ### Group F — 권한 시스템 고도화 (CC 권한 문서 기반) diff --git a/src/AxCopilot/Models/AppSettings.LlmSettings.cs b/src/AxCopilot/Models/AppSettings.LlmSettings.cs index 405d78a..16c1e9f 100644 --- a/src/AxCopilot/Models/AppSettings.LlmSettings.cs +++ b/src/AxCopilot/Models/AppSettings.LlmSettings.cs @@ -360,6 +360,10 @@ public class LlmSettings [JsonPropertyName("enableSkillSystem")] public bool EnableSkillSystem { get; set; } = true; + /// 계층 메모리 시스템 활성화 여부 (AX.md @include 해석 + 경로 범위 규칙). 기본 true. + [JsonPropertyName("enableMemorySystem")] + public bool EnableMemorySystem { get; set; } = true; + /// 추가 스킬 폴더 경로. 빈 문자열이면 기본 폴더만 사용. [JsonPropertyName("skillsFolderPath")] public string SkillsFolderPath { get; set; } = ""; diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs index 8a43175..aa672bb 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs @@ -311,6 +311,9 @@ public partial class AgentLoopService // Phase 17-D: paths: glob 패턴 매칭 스킬 자동 주입 InjectPathBasedSkills(result.FilePath, messages); + // Phase 17-E: .ax/rules/*.md 경로 범위 규칙 주입 + _ = InjectPathScopedRulesAsync(result.FilePath, messages, CancellationToken.None); + // ToolResultSizer 적용 var sizedResult = ToolResultSizer.Apply(result.Output ?? "", call.ToolName); messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, sizedResult.Output)); diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.Memory.cs b/src/AxCopilot/Services/Agent/AgentLoopService.Memory.cs new file mode 100644 index 0000000..e2326db --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentLoopService.Memory.cs @@ -0,0 +1,162 @@ +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +/// +/// Phase 17-E: AgentLoopService — 계층 메모리/컨텍스트 고도화 통합. +/// · 4-layer 계층 메모리 (Managed→User→Project→Local) 시스템 메시지 주입 +/// · @include 지시어 재귀 해석 (최대 5단계, 순환 참조 감지) +/// · .ax/rules/*.md paths: 프론트매터 기반 경로 범위 규칙 주입 +/// +public partial class AgentLoopService +{ + // 세션 공유 서비스 인스턴스 (상태 없음 → 정적) + private static readonly HierarchicalMemoryService _hierarchicalMemory = new(); + private static readonly AxMdIncludeResolver _axMdResolver = new(); + + // workFolder별 PathScopedRuleInjector 캐시 (rules 폴더 로드 비용 절감) + private PathScopedRuleInjector? _pathRuleInjector; + private string? _pathRuleInjectorFolder; + + // 시스템 메시지 내 마커 (in-place 교체, 중복 방지) + private const string MemoryMarker = "## 프로젝트 메모리 (계층 컨텍스트)"; + private const string PathRulesMarker = "## 현재 파일에 적용된 경로 규칙"; + + // ───────────────────────────────────────────────────────────────────── + // 세션 시작 시 계층 메모리 주입 + // ───────────────────────────────────────────────────────────────────── + + /// + /// 4-layer 계층 메모리(AX.md 파일군)를 수집하고 @include 지시어를 재귀 해석하여 + /// 시스템 메시지에 주입합니다. 세션 시작 시 한 번만 호출합니다. + /// + internal async Task InjectHierarchicalMemoryAsync( + List messages, string? workFolder, CancellationToken ct) + { + if (!(_settings.Settings.Llm.EnableMemorySystem)) return; + + try + { + // 4-layer 메모리 병합 문자열 생성 + var merged = _hierarchicalMemory.BuildMergedContext(workFolder); + if (string.IsNullOrWhiteSpace(merged)) return; + + // @include 지시어 재귀 해석 + var basePath = string.IsNullOrWhiteSpace(workFolder) + ? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + : workFolder; + + var resolved = await _axMdResolver.ResolveAsync(merged, basePath); + + if (string.IsNullOrWhiteSpace(resolved)) return; + + // 크기 경고 (40,000자 초과 시) + if (AxMdIncludeResolver.IsOversized(resolved)) + LogService.Warn($"[Memory] 계층 메모리 컨텍스트가 40,000자를 초과합니다 ({resolved.Length:N0}자)."); + + var injection = $"{MemoryMarker}\n\n{resolved.TrimEnd()}"; + + // 시스템 메시지에 in-place 삽입 (마커 기반 중복 방지) + var sysMsg = messages.FirstOrDefault(m => m.Role == "system"); + if (sysMsg != null) + { + var idx = sysMsg.Content.IndexOf(MemoryMarker, StringComparison.Ordinal); + sysMsg.Content = idx >= 0 + ? sysMsg.Content[..idx] + injection + : sysMsg.Content + "\n\n" + injection; + } + else + { + messages.Insert(0, new ChatMessage { Role = "system", Content = injection }); + } + + var fileCount = _hierarchicalMemory.CollectAll(workFolder).Count; + EmitEvent(AgentEventType.Thinking, "memory", + $"계층 메모리 주입됨: {fileCount}개 파일, {resolved.Length:N0}자"); + + _ = _eventLog?.AppendAsync(AgentEventLogType.SkillActivated, + System.Text.Json.JsonSerializer.Serialize(new + { + type = "hierarchical_memory", + fileCount, + charCount = resolved.Length, + workFolder = workFolder ?? "" + })); + } + catch (OperationCanceledException) { /* 취소 시 무시 */ } + catch (Exception ex) + { + LogService.Warn($"[Memory] 계층 메모리 주입 실패: {ex.Message}"); + } + } + + // ───────────────────────────────────────────────────────────────────── + // 파일 도구 호출 후 경로 범위 규칙 주입 + // ───────────────────────────────────────────────────────────────────── + + /// + /// 파일 도구 실행 후, 해당 파일에 적용 가능한 .ax/rules/*.md 경로 범위 규칙을 + /// 시스템 메시지에 주입합니다. 마커 기반 in-place 교체로 중복을 방지합니다. + /// + internal async Task InjectPathScopedRulesAsync( + string? filePath, List messages, CancellationToken ct) + { + if (!(_settings.Settings.Llm.EnableMemorySystem)) return; + if (string.IsNullOrWhiteSpace(filePath)) return; + + var workFolder = _settings.Settings.Llm.WorkFolder; + if (string.IsNullOrWhiteSpace(workFolder)) return; + + try + { + // workFolder 변경 시 injector 재생성 및 규칙 재로드 + if (_pathRuleInjector == null || _pathRuleInjectorFolder != workFolder) + { + _pathRuleInjector = new PathScopedRuleInjector(workFolder); + _pathRuleInjectorFolder = workFolder; + await _pathRuleInjector.LoadRulesAsync(); + } + + var activeRules = _pathRuleInjector.GetActiveRulesForFile(filePath); + if (activeRules.Count == 0) return; + + var injection = $"{PathRulesMarker}\n\n{_pathRuleInjector.BuildInjection(activeRules).TrimEnd()}"; + + // 시스템 메시지에 in-place 삽입 (마커 기반 중복 방지) + var sysMsg = messages.FirstOrDefault(m => m.Role == "system"); + if (sysMsg != null) + { + var idx = sysMsg.Content.IndexOf(PathRulesMarker, StringComparison.Ordinal); + sysMsg.Content = idx >= 0 + ? sysMsg.Content[..idx] + injection + : sysMsg.Content + "\n\n" + injection; + } + else + { + messages.Insert(0, new ChatMessage { Role = "system", Content = injection }); + } + + var ruleNames = string.Join(", ", activeRules + .Select(r => string.IsNullOrEmpty(r.Frontmatter.Name) + ? System.IO.Path.GetFileName(r.FilePath) + : r.Frontmatter.Name)); + + EmitEvent(AgentEventType.Thinking, "path_rules", + $"경로 규칙 주입됨: {activeRules.Count}개 ({ruleNames})"); + + _ = _eventLog?.AppendAsync(AgentEventLogType.SkillActivated, + System.Text.Json.JsonSerializer.Serialize(new + { + type = "path_scoped_rules", + filePath, + ruleCount = activeRules.Count, + ruleNames + })); + } + catch (OperationCanceledException) { /* 취소 시 무시 */ } + catch (Exception ex) + { + LogService.Warn($"[Memory] 경로 범위 규칙 주입 실패: {ex.Message}"); + } + } +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index cee74e9..2a0fa0f 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -226,6 +226,9 @@ public partial class AgentLoopService // Phase 17-A: Reflexion — 과거 교훈을 시스템 메시지에 주입 await InjectReflexionContextAsync(messages, userQuery); + // Phase 17-E: 4-layer 계층 메모리 + @include 해석 → 시스템 메시지 주입 + await InjectHierarchicalMemoryAsync(messages, llm.WorkFolder, ct); + try { // ── 플랜 모드 "always": 첫 번째 호출은 계획만 생성 (도구 없이) ──