From f1b1f1604cc568bf2ce05c6dbf4e2382daf1664d Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 00:56:49 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=2017-E]=20=EA=B3=84=EC=B8=B5=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=EB=A6=AC/=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B3=A0=EB=8F=84=ED=99=94=20=E2=80=94=20AgentLoopService=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentLoopService.Memory.cs (신규, 105줄): - InjectHierarchicalMemoryAsync(): 세션 시작 시 4-layer 계층 메모리 수집 (Managed→User→Project→Local AX.md + rules/*.md) AxMdIncludeResolver.ResolveAsync()로 @include 지시어 최대 5단계 재귀 해석 40,000자 초과 시 크기 경고, 마커 기반 in-place 교체(중복 방지) - InjectPathScopedRulesAsync(): 파일 도구 실행 후 .ax/rules/*.md paths: 프론트매터 기반 경로 범위 규칙 주입. workFolder별 PathScopedRuleInjector 캐시 적용 AgentLoopService.cs (편집): - Phase 17-A(Reflexion) 이후 await InjectHierarchicalMemoryAsync() 호출 추가 AgentLoopService.Execution.cs (편집): - InjectPathBasedSkills() 이후 InjectPathScopedRulesAsync() fire-and-forget 추가 AppSettings.LlmSettings.cs (편집): - EnableMemorySystem 설정 추가 (기본 true, json: "enableMemorySystem") docs/AGENT_ROADMAP.md: - Group E 완료 표시 + 구현 내역 기록 (17-E1/E2 완료, 17-E3 차기) 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 --- docs/AGENT_ROADMAP.md | 15 +- .../Models/AppSettings.LlmSettings.cs | 4 + .../Agent/AgentLoopService.Execution.cs | 3 + .../Services/Agent/AgentLoopService.Memory.cs | 162 ++++++++++++++++++ .../Services/Agent/AgentLoopService.cs | 3 + 5 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 src/AxCopilot/Services/Agent/AgentLoopService.Memory.cs 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": 첫 번째 호출은 계획만 생성 (도구 없이) ──