[Phase 17-E] 계층 메모리/컨텍스트 고도화 — AgentLoopService 통합

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 00:56:49 +09:00
parent 1313c65e5e
commit f1b1f1604c
5 changed files with 181 additions and 6 deletions

View File

@@ -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 권한 문서 기반)

View File

@@ -360,6 +360,10 @@ public class LlmSettings
[JsonPropertyName("enableSkillSystem")]
public bool EnableSkillSystem { get; set; } = true;
/// <summary>계층 메모리 시스템 활성화 여부 (AX.md @include 해석 + 경로 범위 규칙). 기본 true.</summary>
[JsonPropertyName("enableMemorySystem")]
public bool EnableMemorySystem { get; set; } = true;
/// <summary>추가 스킬 폴더 경로. 빈 문자열이면 기본 폴더만 사용.</summary>
[JsonPropertyName("skillsFolderPath")]
public string SkillsFolderPath { get; set; } = "";

View File

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

View File

@@ -0,0 +1,162 @@
using AxCopilot.Models;
namespace AxCopilot.Services.Agent;
/// <summary>
/// Phase 17-E: AgentLoopService — 계층 메모리/컨텍스트 고도화 통합.
/// · 4-layer 계층 메모리 (Managed→User→Project→Local) 시스템 메시지 주입
/// · @include 지시어 재귀 해석 (최대 5단계, 순환 참조 감지)
/// · .ax/rules/*.md paths: 프론트매터 기반 경로 범위 규칙 주입
/// </summary>
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 = "## 현재 파일에 적용된 경로 규칙";
// ─────────────────────────────────────────────────────────────────────
// 세션 시작 시 계층 메모리 주입
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// 4-layer 계층 메모리(AX.md 파일군)를 수집하고 @include 지시어를 재귀 해석하여
/// 시스템 메시지에 주입합니다. 세션 시작 시 한 번만 호출합니다.
/// </summary>
internal async Task InjectHierarchicalMemoryAsync(
List<ChatMessage> 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}");
}
}
// ─────────────────────────────────────────────────────────────────────
// 파일 도구 호출 후 경로 범위 규칙 주입
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// 파일 도구 실행 후, 해당 파일에 적용 가능한 .ax/rules/*.md 경로 범위 규칙을
/// 시스템 메시지에 주입합니다. 마커 기반 in-place 교체로 중복을 방지합니다.
/// </summary>
internal async Task InjectPathScopedRulesAsync(
string? filePath, List<ChatMessage> 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}");
}
}
}

View File

@@ -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": 첫 번째 호출은 계획만 생성 (도구 없이) ──