[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:
@@ -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 권한 문서 기반)
|
||||
|
||||
|
||||
@@ -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; } = "";
|
||||
|
||||
@@ -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));
|
||||
|
||||
162
src/AxCopilot/Services/Agent/AgentLoopService.Memory.cs
Normal file
162
src/AxCopilot/Services/Agent/AgentLoopService.Memory.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": 첫 번째 호출은 계획만 생성 (도구 없이) ──
|
||||
|
||||
Reference in New Issue
Block a user