메모리 include 감사 로그와 AX Agent 메모리 상태 UX 강화
- AgentMemoryService에 @include 성공/차단 감사 로그(MemoryInclude) 기록 추가 - Cowork/Code 하단 폴더 바에 메모리 규칙/학습 메모리 상태 요약 표시 추가 - 설정의 외부 메모리 include 안내 문구를 감사 로그 기준으로 정리 - dotnet build 검증 완료 (경고 0 / 오류 0)
This commit is contained in:
@@ -1417,3 +1417,6 @@ MIT License
|
||||
- 업데이트: 2026-04-07 01:00 (KST)
|
||||
- AX Agent 설정의 에이전트 메모리 섹션에 `적용 중 메모리 계층` 요약을 추가했습니다. 현재 컨텍스트에 반영된 계층형 규칙 수와 학습 메모리 수를 한 줄로 보고, 아래에서 활성 규칙 파일의 우선순위·설명·태그를 바로 확인할 수 있습니다.
|
||||
- 메모리 파일을 편집하거나 학습 메모리를 초기화하면 이 요약이 즉시 다시 계산되도록 연결했고, `새로고침` 버튼도 추가해 현재 작업 폴더 기준 메모리 적용 상태를 바로 다시 확인할 수 있게 했습니다.
|
||||
업데이트: 2026-04-07 01:15 (KST)
|
||||
|
||||
- AX Agent 메모리 구조를 추가 강화했습니다. `@include` 확장 시도는 이제 감사 로그에 `MemoryInclude` 항목으로 남고, Cowork/Code 하단 폴더 바에 현재 적용 중인 계층형 메모리/학습 메모리 상태가 요약 표시됩니다.
|
||||
|
||||
@@ -5226,3 +5226,9 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
||||
- [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs)
|
||||
- `RefreshMemoryOverview()`를 추가해 현재 작업 폴더 기준 메모리 계층 적용 상태를 계산해 UI에 표시하도록 했다.
|
||||
- 설정 창 로드 시, 메모리 파일 저장 후, 학습 메모리 초기화 후 모두 이 요약을 자동으로 갱신해 설정 UI와 실제 적용 상태가 어긋나지 않도록 정리했다.
|
||||
업데이트: 2026-04-07 01:15 (KST)
|
||||
|
||||
- 메모리 구조 후속 고도화:
|
||||
- `AgentMemoryService`에서 `@include` 시도 성공/차단을 감사 로그(`MemoryInclude`)로 기록
|
||||
- `ChatWindow` 하단 폴더 바에 메모리 상태 요약(`메모리 n · 학습 n`) 추가
|
||||
- 설정의 `외부 메모리 include 허용` 안내 문구를 감사 로그 기준으로 갱신
|
||||
|
||||
@@ -570,10 +570,11 @@ public class AgentMemoryService
|
||||
|
||||
if (!inCodeBlock && trimmed.StartsWith("@", StringComparison.Ordinal) && trimmed.Length > 1)
|
||||
{
|
||||
var includePath = ResolveIncludePath(fullPath, trimmed, projectRoot, IsExternalMemoryIncludeAllowed());
|
||||
if (!string.IsNullOrWhiteSpace(includePath))
|
||||
var resolution = ResolveIncludePath(fullPath, trimmed, projectRoot, IsExternalMemoryIncludeAllowed());
|
||||
LogIncludeAudit(fullPath, trimmed, resolution);
|
||||
if (!string.IsNullOrWhiteSpace(resolution.ResolvedPath))
|
||||
{
|
||||
var included = ExpandInstructionIncludes(includePath, projectRoot, visited, depth + 1);
|
||||
var included = ExpandInstructionIncludes(resolution.ResolvedPath, projectRoot, visited, depth + 1);
|
||||
if (!string.IsNullOrWhiteSpace(included))
|
||||
{
|
||||
sb.AppendLine(included.TrimEnd());
|
||||
@@ -595,18 +596,18 @@ public class AgentMemoryService
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveIncludePath(string currentFile, string includeDirective, string? projectRoot, bool allowExternal)
|
||||
private static MemoryIncludeResolution ResolveIncludePath(string currentFile, string includeDirective, string? projectRoot, bool allowExternal)
|
||||
{
|
||||
var target = includeDirective[1..].Trim();
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
return null;
|
||||
return MemoryIncludeResolution.CreateFailure("", "빈 include 지시문");
|
||||
|
||||
var hashIndex = target.IndexOf('#');
|
||||
if (hashIndex >= 0)
|
||||
target = target[..hashIndex];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
return null;
|
||||
return MemoryIncludeResolution.CreateFailure("", "주석만 포함된 include 지시문");
|
||||
|
||||
string resolved;
|
||||
var externalCandidate = false;
|
||||
@@ -634,18 +635,20 @@ public class AgentMemoryService
|
||||
if (!string.IsNullOrWhiteSpace(ext) &&
|
||||
!new[] { ".md", ".txt", ".cs", ".json", ".yml", ".yaml", ".xml", ".props", ".targets" }
|
||||
.Contains(ext, StringComparer.OrdinalIgnoreCase))
|
||||
return null;
|
||||
return MemoryIncludeResolution.CreateFailure(resolved, $"지원하지 않는 확장자 {ext}");
|
||||
|
||||
if (!allowExternal)
|
||||
{
|
||||
if (externalCandidate)
|
||||
return null;
|
||||
return MemoryIncludeResolution.CreateFailure(resolved, "외부 include가 비활성화되어 있습니다");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(projectRoot) && !IsSubPathOf(projectRoot, resolved))
|
||||
return null;
|
||||
return MemoryIncludeResolution.CreateFailure(resolved, "프로젝트 바깥 경로는 허용되지 않습니다");
|
||||
}
|
||||
|
||||
return File.Exists(resolved) ? resolved : null;
|
||||
return File.Exists(resolved)
|
||||
? MemoryIncludeResolution.CreateSuccess(resolved)
|
||||
: MemoryIncludeResolution.CreateFailure(resolved, "포함할 파일을 찾지 못했습니다");
|
||||
}
|
||||
|
||||
private static bool IsSubPathOf(string baseDirectory, string candidatePath)
|
||||
@@ -680,6 +683,34 @@ public class AgentMemoryService
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogIncludeAudit(string sourcePath, string directive, MemoryIncludeResolution resolution)
|
||||
{
|
||||
try
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
if (app?.SettingsService?.Settings.Llm.EnableAuditLog != true)
|
||||
return;
|
||||
|
||||
AuditLogService.Log(new AuditEntry
|
||||
{
|
||||
ConversationId = "",
|
||||
Tab = "Memory",
|
||||
Action = "MemoryInclude",
|
||||
ToolName = "memory",
|
||||
Parameters = $"{sourcePath} <= {directive}",
|
||||
Result = resolution.Success
|
||||
? $"허용: {resolution.ResolvedPath}"
|
||||
: $"차단: {resolution.Reason}",
|
||||
FilePath = resolution.ResolvedPath,
|
||||
Success = resolution.Success,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 감사 로그 실패는 메모리 로드에 영향 주지 않음
|
||||
}
|
||||
}
|
||||
|
||||
public string? ReadInstructionFile(string scope, string? workFolder)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -913,3 +944,24 @@ public class MemoryInstructionDocument
|
||||
[JsonPropertyName("priority")]
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class MemoryIncludeResolution
|
||||
{
|
||||
public string? ResolvedPath { get; init; }
|
||||
public string Reason { get; init; } = "";
|
||||
public bool Success { get; init; }
|
||||
|
||||
public static MemoryIncludeResolution CreateSuccess(string path) => new()
|
||||
{
|
||||
ResolvedPath = path,
|
||||
Success = true,
|
||||
Reason = "허용"
|
||||
};
|
||||
|
||||
public static MemoryIncludeResolution CreateFailure(string? path, string reason) => new()
|
||||
{
|
||||
ResolvedPath = string.IsNullOrWhiteSpace(path) ? null : path,
|
||||
Success = false,
|
||||
Reason = reason
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ public partial class ChatWindow
|
||||
LoadCompactionMetricsFromConversation();
|
||||
UpdatePermissionUI();
|
||||
UpdateDataUsageUI();
|
||||
UpdateMemoryStatusUi();
|
||||
RefreshContextUsageVisual();
|
||||
ScheduleGitBranchRefresh();
|
||||
UpdateGitBranchUi(_currentGitBranchName, GitBranchFilesText?.Text ?? "", GitBranchAddedText?.Text ?? "", GitBranchDeletedText?.Text ?? "", _currentGitTooltip ?? "", BtnGitBranch?.Visibility ?? Visibility.Collapsed);
|
||||
@@ -86,6 +87,61 @@ public partial class ChatWindow
|
||||
_folderDataUsage = GetAutomaticFolderDataUsage();
|
||||
}
|
||||
|
||||
private void UpdateMemoryStatusUi()
|
||||
{
|
||||
if (BtnMemoryStatus == null || MemoryStatusLabel == null)
|
||||
return;
|
||||
|
||||
if (_activeTab == "Chat")
|
||||
{
|
||||
BtnMemoryStatus.Visibility = Visibility.Collapsed;
|
||||
MemoryStatusSeparator.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var memory = app?.MemoryService;
|
||||
if (memory == null || !_settings.Settings.Llm.EnableAgentMemory)
|
||||
{
|
||||
MemoryStatusLabel.Text = "메모리 꺼짐";
|
||||
BtnMemoryStatus.ToolTip = "에이전트 메모리가 비활성화되어 있습니다.";
|
||||
BtnMemoryStatus.Visibility = Visibility.Visible;
|
||||
MemoryStatusSeparator.Visibility = Visibility.Visible;
|
||||
return;
|
||||
}
|
||||
|
||||
var workFolder = GetCurrentWorkFolder();
|
||||
memory.Load(workFolder);
|
||||
var docs = memory.InstructionDocuments;
|
||||
var learned = memory.All.Count;
|
||||
|
||||
MemoryStatusLabel.Text = docs.Count > 0 || learned > 0
|
||||
? $"메모리 {docs.Count} · 학습 {learned}"
|
||||
: "메모리 없음";
|
||||
|
||||
var lines = docs
|
||||
.Take(4)
|
||||
.Select(doc =>
|
||||
{
|
||||
var priority = doc.Priority > 0 ? $"우선순위 {doc.Priority}" : "우선순위 미정";
|
||||
var description = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" · {doc.Description}";
|
||||
return $"[{doc.Label}] {priority}{description}";
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (docs.Count > lines.Count)
|
||||
lines.Add($"외 {docs.Count - lines.Count}개 규칙");
|
||||
|
||||
var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes
|
||||
? "외부 include 허용"
|
||||
: "외부 include 차단";
|
||||
BtnMemoryStatus.ToolTip = lines.Count == 0
|
||||
? $"계층형 규칙이 없습니다.\n학습 메모리 {learned}개\n{includePolicy}"
|
||||
: $"계층형 규칙 {docs.Count}개 · 학습 메모리 {learned}개\n{string.Join("\n", lines)}\n{includePolicy}";
|
||||
BtnMemoryStatus.Visibility = Visibility.Visible;
|
||||
MemoryStatusSeparator.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void UpdateSelectedPresetGuide(ChatConversation? conversation = null)
|
||||
{
|
||||
if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null)
|
||||
|
||||
@@ -2379,9 +2379,11 @@
|
||||
<ColumnDefinition Width="Auto"/> <!-- 4: 구분선 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 5: 데이터 활용 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 6: 구분선 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 7: 권한 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 7: 메모리 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 8: 구분선 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 9: git -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 9: 권한 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 10: 구분선 -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- 11: git -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 폴더 아이콘 -->
|
||||
@@ -2412,8 +2414,27 @@
|
||||
|
||||
<!-- 폴더 데이터 활용은 코드/코워크 자동 정책으로만 동작 -->
|
||||
|
||||
<Border x:Name="MemoryStatusSeparator" Grid.Column="8" Width="1" Height="18" Margin="4,0"
|
||||
Visibility="Collapsed"
|
||||
Background="{DynamicResource SeparatorColor}"/>
|
||||
|
||||
<Button x:Name="BtnMemoryStatus" Grid.Column="7" Style="{StaticResource FooterChipBtn}"
|
||||
Padding="10,5"
|
||||
Visibility="Collapsed"
|
||||
BorderThickness="0"
|
||||
ToolTip="현재 적용된 메모리 상태">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock x:Name="MemoryStatusLabel" Text="메모리 없음" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 권한 메뉴 -->
|
||||
<Button x:Name="BtnPermission" Grid.Column="7" Style="{StaticResource FooterChipBtn}"
|
||||
<Button x:Name="BtnPermission" Grid.Column="9" Style="{StaticResource FooterChipBtn}"
|
||||
Padding="10,5" Click="BtnPermission_Click" ToolTip="파일 접근 권한"
|
||||
BorderThickness="0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
@@ -2426,12 +2447,12 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Border x:Name="GitBranchSeparator" Grid.Column="8" Width="1" Height="18" Margin="4,0"
|
||||
<Border x:Name="GitBranchSeparator" Grid.Column="10" Width="1" Height="18" Margin="4,0"
|
||||
Visibility="Collapsed"
|
||||
Background="{DynamicResource SeparatorColor}"/>
|
||||
|
||||
<Button x:Name="BtnGitBranch"
|
||||
Grid.Column="9"
|
||||
Grid.Column="11"
|
||||
Style="{StaticResource FooterChipBtn}"
|
||||
Padding="10,5"
|
||||
Margin="2,0,0,0"
|
||||
|
||||
@@ -4940,7 +4940,7 @@
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="외부 메모리 include 허용"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="AXMEMORY.md의 @include가 프로젝트 바깥 파일을 읽는 것을 허용합니다. 기본은 안전하게 꺼져 있습니다."/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="AXMEMORY.md의 @include가 프로젝트 바깥 파일을 읽는 것을 허용합니다. 기본은 안전하게 꺼져 있으며, include 시도는 감사 로그에 기록됩니다."/>
|
||||
</StackPanel>
|
||||
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="{Binding AllowExternalMemoryIncludes, Mode=TwoWay}"/>
|
||||
|
||||
Reference in New Issue
Block a user