AgentLoopService.Skills.cs (신규, 95줄): - InjectPathBasedSkills(): 파일 도구 성공 후 filePath로 GlobMatcher 매칭 → 매칭 스킬 시스템 프롬프트를 시스템 메시지에 in-place 주입 → SkillActivated JSONL 이벤트 로그 기록 - RunSkillInForkAsync(): context:fork 스킬 격리 LLM 실행 (도구 없음) → SkillCompleted JSONL 이벤트 로그 기록 SkillManagerTool.cs: - SetForkRunner(Func<SkillDefinition, string, CancellationToken, Task<string>>) 추가 - exec 액션 + arguments 파라미터 추가 - ExecSkillAsync(): PrepareSkillBodyAsync 인자 치환 → IsForkContext=true: fork runner 호출 → [Fork 스킬 결과] 반환 → 일반 스킬: 시스템 프롬프트 + 준비된 본문 반환 AgentLoopService.cs: - 생성자: SkillManagerTool.SetForkRunner(RunSkillInForkAsync) 주입 AgentLoopService.Execution.cs: - 도구 성공 직후 InjectPathBasedSkills(result.FilePath, messages) 호출 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
200 KiB
AX Copilot 차기 개발 계획 (v1.7.1 ~ v2.0) — 구현 수준 상세 명세
개정 기준: 2026년 4월 — Claude Code 내부 아키텍처 문서 심층 분석 + 구현 수준 상세화 각 Phase는 C# 인터페이스·메서드 시그니처·WPF 바인딩·설정 스키마·통합 포인트까지 명시 현재 v1.7.1 — 52개 에이전트 도구, 29개 내장 스킬, 20개 코워크 프리셋
Claude Code 아키텍처 갭 진단 (v1.7.1 기준)
| 갭 영역 | CC 보유 기능 | AX 현재 | 우선순위 Phase |
|---|---|---|---|
| 훅 이벤트 | 17종 이벤트, 4타입(command/http/prompt/agent) | ~8종, 2타입 | 17-C |
| 훅 출력 | additionalContext·permissionDecision·updatedInput 주입 | 없음 | 17-C |
| 스킬 격리 | context:fork 서브에이전트 컨텍스트 | 없음 | 17-D |
| 스킬 경로 활성화 | paths: glob 패턴 자동 주입 | 없음 | 17-D |
| @include 메모리 | @파일경로 5단계 포함 | 없음 | 17-E |
| 경로 기반 규칙 | rules/*.md paths: 프론트매터 | 없음 | 17-E |
| acceptEdits 모드 | 파일 편집 자동승인, bash 확인 유지 | 없음 | 17-F |
| 패턴 권한 규칙 | Bash(git *) 패턴 허용/차단 | 없음 | 17-F |
| 코디네이터 에이전트 | 계획/라우팅 전담, 구현 위임 | 없음 | 18-A |
| Worktree 격리 | 서브에이전트 독립 git 워킹카피 | 없음 | 18-A |
| 백그라운드 에이전트 | 비동기 실행 + 완료 알림 | 없음 | 18-A |
| Chat UI | 3패널, 인라인 설정, 세션 헤더 바 | 단일 패널, 별도 설정창 | 17-UI |
Phase 17-UI — AX Agent 채팅 화면 전면 개편 (v1.8.0)
목표: ChatGPT Codex 스타일 3패널 레이아웃으로 채팅 화면 완전 재설계. 에이전트 설정을 SettingsWindow에서 채팅 화면 인라인 패널로 이전. Chat/Cowork/Code 탭 및 프리셋 유지.
17-UI-1: 신규 파일 구조
src/AxCopilot/Views/AgentWindow/
AgentWindow.xaml ← 기존 AgentChatWindow 대체
AgentWindow.xaml.cs
AgentSidebarView.xaml ← 좌측 사이드바 (240px↔48px)
AgentSidebarView.xaml.cs
AgentSessionHeaderBar.xaml ← 상단 세션 헤더 바
AgentSessionHeaderBar.xaml.cs
AgentInlineSettingsPanel.xaml ← 우측 슬라이드인 설정 패널 (300px)
AgentInlineSettingsPanel.xaml.cs
AgentInputArea.xaml ← 하단 입력 영역
AgentInputArea.xaml.cs
AgentChatView.xaml ← 채팅 메시지 영역
AgentChatView.xaml.cs
AgentDiffPanel.xaml ← 통합 diff 뷰
src/AxCopilot/ViewModels/
AgentWindowViewModel.cs ← 신규
AgentSidebarViewModel.cs ← 신규
AgentSessionHeaderViewModel.cs ← 신규
AgentInlineSettingsViewModel.cs ← 신규
17-UI-2: AgentWindow.xaml 레이아웃 구조
<!-- 전체 레이아웃: Grid 3열 -->
<Window x:Class="AxCopilot.Views.AgentWindow.AgentWindow"
Style="{StaticResource ThemedWindow}">
<Grid>
<Grid.ColumnDefinitions>
<!-- 사이드바: 240px 또는 48px (애니메이션) -->
<ColumnDefinition x:Name="SidebarColumn" Width="240"/>
<!-- 메인 영역 -->
<ColumnDefinition Width="*"/>
<!-- 인라인 설정 패널: 0 또는 300px (슬라이드) -->
<ColumnDefinition x:Name="SettingsColumn" Width="0"/>
</Grid.ColumnDefinitions>
<!-- 좌측 사이드바 -->
<views:AgentSidebarView Grid.Column="0"
DataContext="{Binding Sidebar}"
IsExpanded="{Binding IsSidebarExpanded}"/>
<!-- 메인 영역: 상단 헤더 + 채팅 + 입력 -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="48"/> <!-- 세션 헤더 바 -->
<RowDefinition Height="*"/> <!-- 채팅 뷰 -->
<RowDefinition Height="Auto"/> <!-- 입력 영역 -->
</Grid.RowDefinitions>
<views:AgentSessionHeaderBar Grid.Row="0"
DataContext="{Binding SessionHeader}"/>
<views:AgentChatView Grid.Row="1"
ItemsSource="{Binding Messages}"/>
<views:AgentInputArea Grid.Row="2"
DataContext="{Binding InputArea}"/>
</Grid>
<!-- 우측 인라인 설정 패널 -->
<views:AgentInlineSettingsPanel Grid.Column="2"
DataContext="{Binding InlineSettings}"
Visibility="{Binding IsSettingsPanelOpen, Converter={...}}"/>
</Grid>
</Window>
애니메이션 스펙:
- 사이드바 확장/축소:
DoubleAnimationDuration=0:0:0.2EasingFunction=CubicEase EaseInOut- Width: 240 ↔ 48 (GridLength 직접 애니메이션,
GridLengthAnimation헬퍼 클래스 사용)
- Width: 240 ↔ 48 (GridLength 직접 애니메이션,
- 설정 패널 슬라이드:
DoubleAnimationDuration=0:0:0.2EasingFunction=CubicEase EaseOut- Width: 0 ↔ 300
- 아이콘 회전 (사이드바 토글 화살표):
RotateTransform0 ↔ 180도
17-UI-3: AgentWindowViewModel
public class AgentWindowViewModel : ViewModelBase
{
// ── 패널 상태 ──
private bool _isSidebarExpanded = true;
public bool IsSidebarExpanded
{
get => _isSidebarExpanded;
set { SetProperty(ref _isSidebarExpanded, value); OnSidebarToggled(); }
}
private bool _isSettingsPanelOpen;
public bool IsSettingsPanelOpen
{
get => _isSettingsPanelOpen;
set => SetProperty(ref _isSettingsPanelOpen, value);
}
// ── 자식 ViewModel ──
public AgentSidebarViewModel Sidebar { get; }
public AgentSessionHeaderViewModel SessionHeader { get; }
public AgentInlineSettingsViewModel InlineSettings { get; }
public AgentInputAreaViewModel InputArea { get; }
public ObservableCollection<ChatMessageViewModel> Messages { get; }
// ── 커맨드 ──
public ICommand ToggleSidebarCommand { get; } // IsSidebarExpanded 토글
public ICommand ToggleSettingsPanelCommand { get; } // IsSettingsPanelOpen 토글
public ICommand NewSessionCommand { get; }
public ICommand SendMessageCommand { get; }
public ICommand InterruptCommand { get; }
// ── 의존성 ──
public AgentWindowViewModel(
AgentLoopService agentLoop,
SkillLoaderService skillLoader,
AgentMemoryService memory,
SettingsService settings,
IAgentSessionRepository sessionRepo)
{ ... }
// ── 기존 AgentChatWindow 통합 ──
// AgentLoopService.MessageReceived → Messages 추가
// AgentLoopService.ToolExecuting → SessionHeader.IsRunning = true
// AgentLoopService.Completed → SessionHeader.IsRunning = false
private void WireAgentEvents() { ... }
}
17-UI-4: AgentSessionHeaderViewModel
public class AgentSessionHeaderViewModel : ViewModelBase
{
// ── 모델 선택 ──
public ObservableCollection<string> AvailableModels { get; }
private string _selectedModel;
public string SelectedModel
{
get => _selectedModel;
set { SetProperty(ref _selectedModel, value); ApplyModelChange(); }
}
// ── 모드 상태 ──
public PlanMode PlanMode { get; set; } // Off | Always | Auto
public PermissionMode PermissionMode { get; set; } // Default | AcceptEdits | Plan | Bypass
public AgentTab ActiveTab { get; set; } // Chat | Cowork | Code
public string ActivePreset { get; set; }
// ── 실행 상태 ──
public bool IsRunning { get; set; }
public string StatusText { get; set; } // "도구 실행 중...", "생각 중..."
public int CurrentIteration { get; set; }
public int MaxIterations { get; set; }
// ── 도구 상태 아이콘 (우측 상단 아이콘 행) ──
// MCP 연결됨, 훅 활성, 스킬 N개 로드됨 등을 아이콘으로 표시
public bool IsMcpConnected { get; set; }
public int ActiveHookCount { get; set; }
public int LoadedSkillCount { get; set; }
// ── 커맨드 ──
public ICommand ChangePlanModeCommand { get; } // 클릭 시 Off→Always→Auto 순환
public ICommand ChangePermissionModeCommand { get; }
public ICommand ChangeTabCommand { get; } // Chat/Cowork/Code
public ICommand ChangePresetCommand { get; }
public ICommand ToggleSettingsCommand { get; } // AgentWindowViewModel.ToggleSettingsPanelCommand 위임
public ICommand InterruptCommand { get; }
public ICommand ExportPdfCommand { get; }
}
XAML 바인딩 예시 (AgentSessionHeaderBar.xaml):
<!-- 탭 선택 -->
<Border Style="{StaticResource TabHeaderStyle}" MouseLeftButtonUp="...">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Chat" FontSize="13"
Foreground="{DynamicResource PrimaryText}"/>
</StackPanel>
</Border>
<!-- 플랜 모드 토글 버튼 -->
<Border Style="{StaticResource HeaderButtonStyle}"
MouseLeftButtonUp="{...}"
Background="{Binding PlanMode, Converter={StaticResource PlanModeToColorConverter}}">
<TextBlock Text="{Binding PlanMode, Converter={StaticResource PlanModeToLabelConverter}}"
FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
<!-- 실행 상태 스피너 + 텍스트 -->
<StackPanel Orientation="Horizontal"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisConverter}}">
<controls:SpinnerIcon Width="16" Height="16"/>
<TextBlock Text="{Binding StatusText}" FontSize="12"
Foreground="{DynamicResource SecondaryText}" Margin="6,0,0,0"/>
</StackPanel>
17-UI-5: AgentSidebarViewModel
public class AgentSidebarViewModel : ViewModelBase
{
// ── 대화 이력 ──
public ObservableCollection<SessionSummaryViewModel> RecentSessions { get; }
public SessionSummaryViewModel? SelectedSession { get; set; }
// ── 프리셋 목록 ──
public ObservableCollection<PresetViewModel> Presets { get; }
public PresetViewModel? ActivePreset { get; set; }
// ── 확장 상태 ──
// IsExpanded = true: 240px (아이콘+라벨), false: 48px (아이콘만)
public bool IsExpanded { get; set; }
// ── 커맨드 ──
public ICommand NewSessionCommand { get; }
public ICommand SelectSessionCommand { get; } // param: SessionSummaryViewModel
public ICommand DeleteSessionCommand { get; }
public ICommand SelectPresetCommand { get; }
public ICommand EditPresetCommand { get; }
public ICommand ImportSkillCommand { get; }
}
17-UI-6: AgentInlineSettingsPanel (SettingsWindow 분리)
SettingsWindow에서 이전되는 항목:
[이전 대상 — AgentInlineSettingsPanel]
- LLM 서비스 선택 (Chat/Cowork/Code별)
- 모델 선택 (탭별)
- 플랜 모드 (Off/Always/Auto)
- 권한 모드 (Default/AcceptEdits/Plan)
- MaxAgentIterations (탭별)
- MaxRetryOnError
- EnablePostToolVerification (탭별)
- EnableCoworkVerification / EnableCodeVerification
- 컨텍스트 파일 포함 설정
- 시스템 프롬프트 미리보기/편집
- 활성 프리셋 편집
[SettingsWindow에 남는 항목]
- 일반: 테마, 폰트, AI 활성화 토글
- 런처: 단축키, 독 바, Everything 경로
- MCP 서버 등록 (전문 설정)
- 훅 규칙 편집기 (전문 설정)
- 권한 패턴 규칙 편집기 (전문 설정)
- LSP 설정
- 암호화/보안 설정
public class AgentInlineSettingsViewModel : ViewModelBase
{
// ── 탭별 LLM 설정 (ActiveTab에 따라 바인딩 전환) ──
public string LlmService { get; set; }
public string ModelName { get; set; }
public int MaxIterations { get; set; }
public bool EnableVerification { get; set; }
public PlanMode PlanMode { get; set; }
public PermissionMode PermissionMode { get; set; }
// ── 시스템 프롬프트 미리보기 ──
public string SystemPromptPreview { get; set; } // 처음 200자
public ICommand EditSystemPromptCommand { get; } // 전체 편집 팝업
// ── 활성 스킬 목록 ──
public ObservableCollection<SkillChipViewModel> ActiveSkills { get; }
public ICommand ManageSkillsCommand { get; }
// ── MCP 상태 요약 ──
public ObservableCollection<McpStatusChipViewModel> McpServers { get; }
// 설정 변경 즉시 SettingsService.Save() 호출
// 탭 전환 시 해당 탭 설정으로 바인딩 교체
public void SwitchToTab(AgentTab tab) { ... }
}
17-UI-7: AgentInputArea
public class AgentInputAreaViewModel : ViewModelBase
{
public string InputText { get; set; }
public bool IsMultiline { get; set; } // Shift+Enter로 전환
public ObservableCollection<AttachedFileViewModel> AttachedFiles { get; }
public bool IsAtMentionOpen { get; set; } // @ 입력 시 팝업
public bool IsSlashMenuOpen { get; set; } // / 입력 시 스킬 메뉴
public ObservableCollection<SkillSuggestionViewModel> SlashSuggestions { get; }
// 커맨드
public ICommand SendCommand { get; }
public ICommand AttachFileCommand { get; }
public ICommand ClearAttachmentsCommand { get; }
public ICommand OpenSlashMenuCommand { get; } // / 키 감지
public ICommand InsertAtMentionCommand { get; } // @ 키 감지
}
XAML (AgentInputArea.xaml) 핵심:
<Border Background="{DynamicResource ItemBackground}" CornerRadius="12"
Margin="12,0,12,12" Padding="12,8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- 첨부파일 칩 행 -->
<RowDefinition Height="Auto"/> <!-- 텍스트 입력 행 -->
<RowDefinition Height="Auto"/> <!-- 하단 버튼 행 -->
</Grid.RowDefinitions>
<!-- 첨부파일 칩 (파일 있을 때만 표시) -->
<ItemsControl Grid.Row="0" ItemsSource="{Binding AttachedFiles}"
Visibility="{Binding AttachedFiles.Count, Converter={...}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!-- 텍스트 입력 -->
<TextBox Grid.Row="1" x:Name="InputBox"
Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
AcceptsReturn="{Binding IsMultiline}"
MaxHeight="200" TextWrapping="Wrap"
Background="Transparent" BorderThickness="0"
Foreground="{DynamicResource PrimaryText}" FontSize="14"
PreviewKeyDown="InputBox_PreviewKeyDown"/>
<!-- 하단 버튼 행: 📎 파일 | /스킬 | @ 멘션 | [모델칩] | [보내기] -->
<Grid Grid.Row="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<!-- 파일 첨부 -->
<Border Style="{StaticResource InputActionButton}"
MouseLeftButtonUp="...">
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
Foreground="#F59E0B"/>
</Border>
<!-- 슬래시 스킬 -->
<Border Style="{StaticResource InputActionButton}"
MouseLeftButtonUp="...">
<TextBlock Text="/" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource AccentColor}"/>
</Border>
</StackPanel>
<!-- 모델 칩 + 보내기 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Border Style="{StaticResource ModelChipStyle}">
<TextBlock Text="{Binding ModelName}" FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
</Border>
<Border Style="{StaticResource SendButtonStyle}"
MouseLeftButtonUp="...">
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
Foreground="{DynamicResource AccentColor}"/>
</Border>
</StackPanel>
</Grid>
</Grid>
</Border>
17-UI-8: 통합 포인트
| 기존 클래스 | 변경 내용 |
|---|---|
AgentChatWindow.xaml.cs |
AgentWindow로 교체 (리네임 + 재구성). App.xaml.cs의 OpenAgentWindow() 호출부 교체 |
AgentChatViewModel.cs |
AgentWindowViewModel로 흡수·통합 |
SettingsWindow.xaml |
AX Agent 탭 내 에이전트 설정 섹션 제거, "채팅 화면에서 설정 가능" 안내 문구 추가 |
AgentLoopService.cs |
이벤트 구독 포인트 변경 없음. ViewModel에서 이벤트 연결 방식만 변경 |
App.xaml.cs |
_agentWindow 필드를 AgentWindow 타입으로 변경 |
17-UI-9: 구현 순서
1. AgentWindowViewModel + AgentSessionHeaderViewModel (VM 먼저)
2. AgentWindow.xaml 레이아웃 골격 (3열 Grid, 빈 자식)
3. AgentSessionHeaderBar.xaml (탭, 모드, 상태)
4. AgentSidebarView.xaml (대화 목록, 프리셋)
5. 기존 AgentChatView 내용 이식 (메시지 버블 등)
6. AgentInputArea.xaml (입력창, 첨부, 슬래시 메뉴)
7. AgentInlineSettingsPanel.xaml + SettingsWindow 항목 이전
8. 사이드바/설정 패널 애니메이션
9. GridLengthAnimation 헬퍼 클래스 구현
10. App.xaml.cs OpenAgentWindow() 교체 및 전체 통합 테스트
Phase 17-A — Reflexion 강화 (v1.8.0)
목표: 성공·실패 모두 구조화된 자기평가 저장 → 동일 작업 유형 재실행 시 자동 참고
17-A-1: 핵심 신규 클래스
// 반성 메모리 엔트리
public record ReflexionEntry
{
public string TaskType { get; init; } // "code_generation", "file_refactor" 등
public string Summary { get; init; } // 작업 요약
public bool IsSuccess { get; init; }
public float CompletionScore { get; init; } // 0.0~1.0
public string[] Weaknesses { get; init; } // 부족한 점 (실패 시)
public string[] Strengths { get; init; } // 잘된 점 (성공 시)
public string[] Lessons { get; init; } // 다음에 적용할 교훈
public DateTime CreatedAt { get; init; }
public string SessionId { get; init; }
}
// 반성 메모리 저장소 인터페이스
public interface IReflexionRepository
{
Task SaveAsync(ReflexionEntry entry);
Task<IReadOnlyList<ReflexionEntry>> GetByTaskTypeAsync(string taskType, int limit = 5);
Task<string> BuildContextPromptAsync(string taskType);
// 저장 위치: %APPDATA%\AxCopilot\reflexion\<taskType>.jsonl
}
// 반성 메모리 구현
public class JsonlReflexionRepository : IReflexionRepository
{
private readonly string _baseDir; // %APPDATA%\AxCopilot\reflexion\
public Task SaveAsync(ReflexionEntry entry) { ... }
public Task<IReadOnlyList<ReflexionEntry>> GetByTaskTypeAsync(string taskType, int limit = 5) { ... }
public Task<string> BuildContextPromptAsync(string taskType)
{
// 최근 5개 엔트리를 시스템 프롬프트 삽입용 텍스트로 조합
// "이전 유사 작업에서 배운 점: ..."
}
}
// 자기평가 생성 서비스
public class ReflexionEvaluatorService
{
private readonly ILlmClient _llm;
private readonly IReflexionRepository _repo;
// AgentLoopService.Completed 이벤트에서 호출
public Task<ReflexionEntry> EvaluateAsync(
AgentSession session,
AgentResult result,
CancellationToken ct);
// 평가 프롬프트: 완성도 점수, 강점/약점, 교훈 3가지 추출
private string BuildEvaluationPrompt(AgentSession session, AgentResult result) { ... }
}
// 작업 유형 분류기
public class TaskTypeClassifier
{
// 사용자 입력 → 작업 유형 분류 (규칙 기반 + LLM 보조)
public string Classify(string userMessage) { ... }
// "파일 수정", "코드 생성", "문서 작성", "검색", "분석", "테스트" 등
private static readonly Dictionary<string, string[]> _keywords = ...;
}
17-A-2: AgentLoopService 통합 포인트
// AgentLoopService.cs 에 추가
public class AgentLoopService
{
// 기존 필드에 추가
private readonly ReflexionEvaluatorService _reflexion;
private readonly IReflexionRepository _reflexionRepo;
private readonly TaskTypeClassifier _taskClassifier;
// RunAsync() 시작 시: 반성 컨텍스트 주입
private async Task<string> BuildSystemPromptAsync(string userMessage)
{
var taskType = _taskClassifier.Classify(userMessage);
var reflexionContext = await _reflexionRepo.BuildContextPromptAsync(taskType);
// 기존 시스템 프롬프트 끝에 reflexionContext 추가
}
// RunAsync() 완료 후: 자기평가 저장
private async Task OnSessionCompletedAsync(AgentSession session, AgentResult result)
{
var entry = await _reflexion.EvaluateAsync(session, result, CancellationToken.None);
await _reflexionRepo.SaveAsync(entry);
}
}
17-A-3: 검증 대상 확대 (17-A2)
// 기존 PostToolVerificationService 확장
public class PostToolVerificationService
{
// 기존: bash, process 도구 후 검증
// 추가: script_create, file_edit 도구 후 검증
private static readonly HashSet<string> _verifiableTools = new()
{
"bash", "process",
"script_create", // 신규
"file_edit", // 신규
"file_write", // 신규
};
// 검증 체크리스트 (사용자 편집 가능)
// 저장: %APPDATA%\AxCopilot\verification_checklist.json
public Task<VerificationChecklist> LoadChecklistAsync() { ... }
public Task SaveChecklistAsync(VerificationChecklist checklist) { ... }
}
public record VerificationChecklist
{
[JsonPropertyName("code_checks")]
public string[] CodeChecks { get; init; } = new[]
{
"구문 오류 없음",
"참조 무결성 확인",
"null 참조 없음"
};
[JsonPropertyName("file_checks")]
public string[] FileChecks { get; init; } = new[]
{
"파일이 실제로 수정됨",
"인코딩 올바름 (UTF-8)"
};
}
17-A-4: 설정 스키마 변경
// AppSettings.cs → LlmSettings 에 추가
public class LlmSettings
{
// ... 기존 ...
[JsonPropertyName("enable_reflexion")]
public bool EnableReflexion { get; set; } = true;
[JsonPropertyName("reflexion_max_context_entries")]
public int ReflexionMaxContextEntries { get; set; } = 5;
[JsonPropertyName("reflexion_evaluate_on_success")]
public bool ReflexionEvaluateOnSuccess { get; set; } = true;
}
Phase 17-B — 구조화된 태스크 상태 + 이벤트 로그 (v1.8.0)
목표: 대화 압축 시에도 유지되는 Working Memory + JSONL 이벤트 로그
17-B-1: TaskState (Working Memory)
// 세션 전체에 걸쳐 유지되는 구조화된 작업 상태
public class TaskState
{
public string SessionId { get; init; }
public string CurrentTask { get; set; } // 현재 수행 중인 작업 요약
public List<string> ReferencedFiles { get; set; } // 이번 세션에서 참조한 파일들
public List<DecisionLogEntry> DecisionLog { get; set; } // 의사결정 이력
public Dictionary<string, string> KeyFacts { get; set; } // 핵심 사실 (key→value)
public string ContextSummary { get; set; } // 대화 압축 시 갱신되는 요약
// 직렬화: %APPDATA%\AxCopilot\sessions\<sessionId>\task_state.json
public Task SaveAsync(string baseDir) { ... }
public static Task<TaskState> LoadAsync(string sessionId, string baseDir) { ... }
}
public record DecisionLogEntry
{
public DateTime Timestamp { get; init; }
public string Decision { get; init; }
public string Reason { get; init; }
public string[] Alternatives { get; init; }
}
// TaskState 관리 서비스
public class TaskStateService
{
private readonly string _baseDir;
private TaskState? _current;
public TaskState Current => _current ?? throw new InvalidOperationException();
public Task InitializeAsync(string sessionId) { ... }
public Task UpdateCurrentTaskAsync(string taskDescription) { ... }
public Task AddReferencedFileAsync(string filePath) { ... }
public Task LogDecisionAsync(string decision, string reason, string[] alternatives) { ... }
public Task UpdateContextSummaryAsync(string summary) { ... }
// AgentLoopService.ContextCompacting 이벤트에서 호출
// 압축 전 현재 TaskState를 시스템 프롬프트에 추가
public string BuildCompactContextInjection() { ... }
}
17-B-2: Event-Sourced 이벤트 로그
// 모든 에이전트 이벤트의 구조화된 기록
public record AgentEventRecord
{
[JsonPropertyName("seq")]
public long SeqNo { get; init; }
[JsonPropertyName("id")]
public string EventId { get; init; } // GUID
[JsonPropertyName("parent_id")]
public string? ParentEventId { get; init; }
[JsonPropertyName("session_id")]
public string SessionId { get; init; }
[JsonPropertyName("type")]
public AgentEventType Type { get; init; }
[JsonPropertyName("ts")]
public DateTime Timestamp { get; init; }
[JsonPropertyName("payload")]
public JsonElement Payload { get; init; } // 이벤트별 페이로드
}
public enum AgentEventType
{
SessionStart, SessionEnd,
UserMessage, AssistantMessage,
ToolRequest, ToolResult,
HookFired, HookResult,
SkillActivated, SkillCompleted,
CompactionTriggered, CompactionCompleted,
SubagentSpawned, SubagentCompleted,
Error
}
// JSONL 기반 이벤트 로그 저장소
// 저장: %APPDATA%\AxCopilot\sessions\<sessionId>\events.jsonl
public class AgentEventLog
{
private readonly string _filePath;
private long _seq = 0;
public Task AppendAsync(AgentEventType type, object payload, string? parentId = null) { ... }
public IAsyncEnumerable<AgentEventRecord> ReadAllAsync() { ... }
public IAsyncEnumerable<AgentEventRecord> ReadFromAsync(long fromSeq) { ... }
}
// AgentLoopService에서 이벤트 기록
public class AgentLoopService
{
private AgentEventLog? _eventLog;
// 기존 _events 컬렉션 대신 또는 병렬로 이벤트 로그 기록
private async Task RecordEventAsync(AgentEventType type, object payload)
{
if (_eventLog != null)
await _eventLog.AppendAsync(type, payload);
}
}
17-B-3: 설정 스키마 변경
// AppSettings.cs 에 추가
public class AppSettings
{
// ... 기존 ...
[JsonPropertyName("enable_event_log")]
public bool EnableEventLog { get; set; } = true;
[JsonPropertyName("event_log_retention_days")]
public int EventLogRetentionDays { get; set; } = 30;
[JsonPropertyName("enable_task_state")]
public bool EnableTaskState { get; set; } = true;
}
Phase 17-C — 훅 시스템 고도화 (v1.8.0)
목표: Claude Code의 17종 이벤트·4타입 훅 시스템을 AX Copilot에 이식
17-C-1: 훅 이벤트 열거형 확장
// 기존 HookEvent 열거형에 추가
public enum HookEvent
{
// ── 기존 (유지) ──
PreToolUse,
PostToolUse,
AgentStop,
// ── 신규 (Phase 17-C) ──
UserPromptSubmit, // 사용자 프롬프트 제출 전 (차단·수정 가능)
PreCompact, // 컨텍스트 압축 전
PostCompact, // 컨텍스트 압축 후
FileChanged, // watchPaths에 등록된 파일 변경 시
CwdChanged, // 작업 디렉토리 변경 시 (프로젝트 전환 감지)
SessionStart, // 세션 시작 (watchPaths 등록, 초기 컨텍스트 주입)
SessionEnd, // 세션 종료 (정리 작업)
ConfigChange, // .ax/rules/*.md 또는 AX.md 파일 변경
PermissionRequest, // 도구 실행 권한 요청 (프로그래밍 방식 승인/거부)
PreSkillExecute, // 스킬 실행 전
PostSkillExecute, // 스킬 실행 후
SubagentStart, // 서브에이전트 시작
SubagentStop, // 서브에이전트 종료
AgentIterationStart, // 에이전트 루프 반복 시작
AgentIterationEnd // 에이전트 루프 반복 종료
}
17-C-2: 훅 타입 인터페이스 (Strategy 패턴)
// 훅 실행기 인터페이스 (Strategy)
public interface IHookExecutor
{
HookType Type { get; }
Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct);
}
// 훅 컨텍스트 (이벤트 정보 + 도구 입력 등)
public record HookContext
{
public HookEvent Event { get; init; }
public string? ToolName { get; init; } // PreToolUse, PostToolUse 시
public string? ToolInput { get; init; } // 도구 입력 JSON
public string? ToolOutput { get; init; } // PostToolUse 시 도구 출력
public string? UserMessage { get; init; } // UserPromptSubmit 시
public string? ChangedFilePath { get; init; } // FileChanged 시
public string SessionId { get; init; }
public Dictionary<string, string> EnvVars { get; init; } // $TOOL_NAME, $ARGUMENTS 등
}
// 훅 결과
public record HookResult
{
public bool Block { get; init; } = false; // true: 도구 실행 차단
public string? BlockReason { get; init; }
public string? AdditionalContext { get; init; } // 시스템 프롬프트에 추가할 텍스트
public string? UpdatedInput { get; init; } // 수정된 도구 입력 (updatedInput)
public PermissionDecision? PermissionDecision { get; init; } // allow | deny
public IReadOnlyList<string>? WatchPaths { get; init; } // SessionStart 시 등록
public string? StatusMessage { get; init; } // 스피너 커스텀 메시지
}
public enum PermissionDecision { Allow, Deny, Ask }
// ── 구현체 ──
// 1. command 타입: 외부 프로세스 실행
public class CommandHookExecutor : IHookExecutor
{
public HookType Type => HookType.Command;
public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
{
// stdout → stdout 읽어서 HookResult 파싱
// exit code 2 → Block = true
// JSON 출력 → AdditionalContext, UpdatedInput 파싱
}
}
// 2. http 타입: HTTP 웹훅
public class HttpHookExecutor : IHookExecutor
{
public HookType Type => HookType.Http;
// POST JSON 페이로드 → 응답 JSON 파싱 → HookResult
}
// 3. prompt 타입: LLM 검사 (소형 모델로 빠른 판단)
public class PromptHookExecutor : IHookExecutor
{
public HookType Type => HookType.Prompt;
private readonly ILlmClientFactory _llmFactory;
public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
{
// 설정된 prompt 템플릿 + 컨텍스트 변수 치환
// LLM 호출 (기본 claude-haiku-4-5, 타임아웃 15초)
// 응답에서 "BLOCK"/"ALLOW" 및 이유 파싱
}
// 훅 설정 필드
public record PromptHookConfig
{
public string Prompt { get; init; }
public string Model { get; init; } = "claude-haiku-4-5";
public int TimeoutSeconds { get; init; } = 15;
}
}
// 4. agent 타입: 미니 에이전트 루프
public class AgentHookExecutor : IHookExecutor
{
public HookType Type => HookType.Agent;
private readonly AgentLoopService _agentLoop;
public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
{
// 격리된 미니 에이전트 루프 실행
// 파일 읽기·명령 실행·검증 가능
// 최대 반복: 3회 (하드코딩 또는 설정)
// 결과를 HookResult로 변환
}
}
// 훅 실행기 팩토리 (Factory 패턴)
public class HookExecutorFactory
{
private readonly IReadOnlyDictionary<HookType, IHookExecutor> _executors;
public HookExecutorFactory(
CommandHookExecutor cmd,
HttpHookExecutor http,
PromptHookExecutor prompt,
AgentHookExecutor agent)
{
_executors = new Dictionary<HookType, IHookExecutor>
{
[HookType.Command] = cmd,
[HookType.Http] = http,
[HookType.Prompt] = prompt,
[HookType.Agent] = agent,
};
}
public IHookExecutor GetExecutor(HookType type)
=> _executors.TryGetValue(type, out var exec) ? exec
: throw new NotSupportedException($"훅 타입 미지원: {type}");
}
17-C-3: 훅 속성 모델 확장
// 기존 HookDefinition 에 속성 추가
public class HookDefinition
{
// ── 기존 ──
public string? Matcher { get; init; }
public HookType Type { get; init; }
public string? Command { get; init; }
public string? Url { get; init; }
// ── 신규 속성 (Phase 17-C) ──
[JsonPropertyName("if")]
public string? Condition { get; init; } // 조건부 실행 (권한 모드 등 체크)
[JsonPropertyName("once")]
public bool Once { get; init; } = false; // 실행 후 자기 제거
[JsonPropertyName("async")]
public bool IsAsync { get; init; } = false; // 비동기 실행 (결과 기다리지 않음)
[JsonPropertyName("async_rewake")]
public bool AsyncRewake { get; init; } = false; // 비동기 완료 후 에이전트 재깨우기
[JsonPropertyName("status_message")]
public string? StatusMessage { get; init; } // 실행 중 스피너 메시지
[JsonPropertyName("timeout")]
public int TimeoutSeconds { get; init; } = 30;
// prompt 타입 전용
[JsonPropertyName("prompt")]
public string? Prompt { get; init; }
[JsonPropertyName("model")]
public string? Model { get; init; }
// agent 타입 전용
[JsonPropertyName("max_iterations")]
public int MaxIterations { get; init; } = 3;
}
17-C-4: HookRunnerService 확장
public class HookRunnerService
{
private readonly HookExecutorFactory _executorFactory;
private readonly HookConfigRepository _configRepo;
private readonly FileWatcherService _fileWatcher; // FileChanged 훅용
// ── 기존 메서드 시그니처 유지 + 새 이벤트 지원 ──
// 훅 실행 (모든 이벤트 공통)
public async Task<HookRunResult> RunAsync(
HookEvent hookEvent,
HookContext context,
CancellationToken ct)
{
var defs = _configRepo.GetHooksForEvent(hookEvent, context.ToolName);
// once 훅: 실행 후 설정에서 제거
// async 훅: 비동기 실행, 결과 기다리지 않음
// asyncRewake 훅: 완료 시 AgentLoopService.ResumeAsync() 호출
// if 조건: EvaluateCondition(def.Condition, context) 통과 시만 실행
}
// FileChanged 감시 시작 (SessionStart 훅 watchPaths에서 호출)
public void RegisterWatchPaths(IReadOnlyList<string> patterns)
{
foreach (var pattern in patterns)
_fileWatcher.Watch(pattern, path =>
RunAsync(HookEvent.FileChanged,
new HookContext { ChangedFilePath = path, ... }, CancellationToken.None));
}
// Once 훅 자기 제거
private void RemoveOnceHook(HookDefinition def) { ... }
}
public record HookRunResult
{
public bool AnyBlocked { get; init; }
public string? CombinedContext { get; init; } // 여러 훅의 AdditionalContext 합산
public string? UpdatedInput { get; init; } // 마지막 updatedInput
public PermissionDecision? PermissionDecision { get; init; }
}
17-C-5: AgentLoopService 통합
// AgentLoopService.cs 에서 새 이벤트 발화 위치:
public class AgentLoopService
{
// 1. 사용자 메시지 수신 시 → UserPromptSubmit 훅
private async Task<string?> OnUserMessageAsync(string message)
{
var ctx = new HookContext { Event = HookEvent.UserPromptSubmit, UserMessage = message, ... };
var result = await _hookRunner.RunAsync(HookEvent.UserPromptSubmit, ctx, _ct);
if (result.AnyBlocked) return null; // 차단
return result.UpdatedInput ?? message; // 수정된 메시지 반환
}
// 2. 세션 시작 시 → SessionStart 훅
private async Task OnSessionStartAsync()
{
var result = await _hookRunner.RunAsync(HookEvent.SessionStart, ..., _ct);
if (result.WatchPaths?.Count > 0)
_hookRunner.RegisterWatchPaths(result.WatchPaths);
}
// 3. 컨텍스트 압축 전후 → PreCompact/PostCompact 훅
private async Task CompactContextAsync()
{
await _hookRunner.RunAsync(HookEvent.PreCompact, ..., _ct);
// ... 실제 압축 ...
await _hookRunner.RunAsync(HookEvent.PostCompact, ..., _ct);
}
// 4. 권한 요청 시 → PermissionRequest 훅
private async Task<PermissionDecision> RequestPermissionAsync(string toolName, string input)
{
var ctx = new HookContext { Event = HookEvent.PermissionRequest, ToolName = toolName, ToolInput = input };
var result = await _hookRunner.RunAsync(HookEvent.PermissionRequest, ctx, _ct);
return result.PermissionDecision ?? PermissionDecision.Ask; // 훅 미설정 시 기존 UI 표시
}
}
17-C-6: 설정 스키마 변경
// AppSettings.cs → LlmSettings에 추가
public class LlmSettings
{
[JsonPropertyName("hooks")]
public HooksConfig Hooks { get; set; } = new();
}
public class HooksConfig
{
[JsonPropertyName("user_prompt_submit")]
public List<HookDefinition> UserPromptSubmit { get; set; } = new();
[JsonPropertyName("pre_compact")]
public List<HookDefinition> PreCompact { get; set; } = new();
[JsonPropertyName("post_compact")]
public List<HookDefinition> PostCompact { get; set; } = new();
[JsonPropertyName("file_changed")]
public List<HookDefinition> FileChanged { get; set; } = new();
[JsonPropertyName("session_start")]
public List<HookDefinition> SessionStart { get; set; } = new();
[JsonPropertyName("session_end")]
public List<HookDefinition> SessionEnd { get; set; } = new();
[JsonPropertyName("permission_request")]
public List<HookDefinition> PermissionRequest { get; set; } = new();
// ... 기존 PreToolUse, PostToolUse, AgentStop 유지 ...
}
17-C-7: 훅 편집기 UI (SettingsWindow에 잔류)
// SettingsWindow.xaml — Hooks 탭 (기존 위치 유지)
// 신규: 훅 타입 선택 드롭다운 (Command/Http/Prompt/Agent)
// 신규: prompt 타입 선택 시 → Prompt 텍스트박스 + Model 드롭다운 표시
// 신규: agent 타입 선택 시 → Prompt 텍스트박스 + MaxIterations 숫자 입력 표시
// 신규: 속성 체크박스 행: [once] [async] [asyncRewake]
// 신규: statusMessage 텍스트 입력
// 신규: condition 텍스트 입력 (고급 섹션)
Phase 17-D — 스킬 시스템 고도화 (v1.8.0)
목표: fork 격리·경로 자동 활성화·스킬 범위 훅·모델 오버라이드 구현
17-D-1: SkillFrontmatter 확장
// 기존 SkillFrontmatter에 필드 추가
public class SkillFrontmatter
{
// ── 기존 ──
[JsonPropertyName("description")]
public string Description { get; init; } = string.Empty;
[JsonPropertyName("when_to_use")]
public string WhenToUse { get; init; } = string.Empty;
[JsonPropertyName("arguments")]
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
// ── 신규 (Phase 17-D) ──
[JsonPropertyName("context")]
public SkillContext Context { get; init; } = SkillContext.Default;
// "fork" → 격리된 서브에이전트 컨텍스트
[JsonPropertyName("paths")]
public IReadOnlyList<string> Paths { get; init; } = Array.Empty<string>();
// glob 패턴 목록: ["**/*.py", "src/**/*.cs"]
// 해당 파일 작업 시 자동 컨텍스트 주입
[JsonPropertyName("model")]
public string? Model { get; init; }
// 스킬별 모델 오버라이드: "claude-opus-4-6", "claude-haiku-4-5" 등
[JsonPropertyName("user_invocable")]
public bool UserInvocable { get; init; } = true;
// false → /스킬 목록에서 숨김, AI가 자동 활성화만 가능
[JsonPropertyName("hooks")]
public SkillHooksConfig? Hooks { get; init; }
// 스킬 실행 중에만 적용되는 훅
}
public enum SkillContext { Default, Fork }
public class SkillHooksConfig
{
[JsonPropertyName("pre_skill_execute")]
public List<HookDefinition> PreSkillExecute { get; init; } = new();
[JsonPropertyName("post_skill_execute")]
public List<HookDefinition> PostSkillExecute { get; init; } = new();
[JsonPropertyName("post_tool_use")]
public List<HookDefinition> PostToolUse { get; init; } = new();
}
17-D-2: PathBasedSkillActivator
// 파일 경로에 따라 적용할 스킬을 자동 선택
public class PathBasedSkillActivator
{
private readonly SkillLoaderService _skillLoader;
// 현재 작업 중인 파일 경로 기반으로 활성 스킬 목록 반환
public IReadOnlyList<LoadedSkill> GetActiveSkillsForFile(string filePath)
{
return _skillLoader.GetAllSkills()
.Where(s => s.Frontmatter.Paths.Any(pattern =>
GlobMatcher.IsMatch(filePath, pattern)))
.ToList();
}
// 활성 스킬의 컨텍스트를 시스템 프롬프트 주입용 텍스트로 빌드
public string BuildSkillContextInjection(IReadOnlyList<LoadedSkill> skills)
{
if (skills.Count == 0) return string.Empty;
var sb = new StringBuilder();
sb.AppendLine("## 현재 파일에 자동 적용된 스킬");
foreach (var skill in skills)
{
sb.AppendLine($"### {skill.Name}");
sb.AppendLine(skill.Body);
}
return sb.ToString();
}
// GlobMatcher: ** 와일드카드, *.ext 패턴 지원
private static class GlobMatcher
{
public static bool IsMatch(string path, string pattern) { ... }
}
}
17-D-3: ForkContextSkillRunner
// fork 컨텍스트 스킬: 격리된 서브에이전트 루프에서 실행
public class ForkContextSkillRunner
{
private readonly AgentLoopService _parentLoop;
public async Task<SkillResult> ExecuteInForkAsync(
LoadedSkill skill,
string userInput,
CancellationToken ct)
{
// 1. 격리된 AgentSession 생성 (부모 대화 히스토리 제외)
// 2. 스킬의 시스템 프롬프트만 포함
// 3. 모델 오버라이드 적용 (skill.Frontmatter.Model)
// 4. 스킬 범위 훅 등록 (skill.Frontmatter.Hooks)
// 5. 미니 에이전트 루프 실행 (최대 10 반복)
// 6. 결과 텍스트 반환 (메인 대화에 reply로 주입)
}
}
public record SkillResult
{
public bool IsSuccess { get; init; }
public string Output { get; init; } = string.Empty;
public string? ErrorMessage { get; init; }
public int Iterations { get; init; }
}
17-D-4: SkillLoaderService 확장
public class SkillLoaderService
{
// 기존 메서드 유지 +
// 신규: UserInvocable=false 스킬을 슬래시 메뉴에서 필터
public IReadOnlyList<LoadedSkill> GetUserInvocableSkills()
=> GetAllSkills().Where(s => s.Frontmatter.UserInvocable).ToList();
// 신규: paths 패턴이 있는 스킬 목록 (PathBasedSkillActivator용)
public IReadOnlyList<LoadedSkill> GetPathScopedSkills()
=> GetAllSkills().Where(s => s.Frontmatter.Paths.Count > 0).ToList();
}
17-D-5: AgentLoopService 통합
public class AgentLoopService
{
private readonly PathBasedSkillActivator _pathActivator;
private readonly ForkContextSkillRunner _forkRunner;
// 도구 실행 후: 작업 파일 경로 감지 → 경로 기반 스킬 자동 주입
private async Task OnFileToolExecutedAsync(string filePath)
{
var skills = _pathActivator.GetActiveSkillsForFile(filePath);
if (skills.Count > 0)
{
var injection = _pathActivator.BuildSkillContextInjection(skills);
_contextBuilder.AddDynamicContext("path_skills", injection);
}
}
// 스킬 실행 시: fork vs default 분기
public async Task<string> ExecuteSkillAsync(LoadedSkill skill, string input, CancellationToken ct)
{
if (skill.Frontmatter.Context == SkillContext.Fork)
{
var result = await _forkRunner.ExecuteInForkAsync(skill, input, ct);
return result.Output;
}
else
{
// 기존 방식: 메인 컨텍스트에서 실행
return await ExecuteSkillInMainContextAsync(skill, input, ct);
}
}
}
17-D-6: 설정 없음 (프론트매터 기반)
스킬 시스템 고도화는 SKILL.md 프론트매터 확장이 핵심. 별도 AppSettings 변경 최소화. SkillLoaderService에서 신규 프론트매터 필드 파싱만 추가.
Phase 17-E — 메모리/컨텍스트 고도화 (v1.8.0)
목표: @include 지시어·경로 기반 규칙 주입·/compact 명령·파일 되감기
17-E-1: AxMdIncludeResolver
// AX.md 및 rules/*.md에서 @파일경로 지시어를 재귀 해석
public class AxMdIncludeResolver
{
private const int MaxDepth = 5;
private const long MaxFileSize = 40_000; // 40,000자 경고 임계값
// @파일경로 지시어 모두 해석하여 최종 텍스트 반환
public async Task<string> ResolveAsync(
string content,
string basePath,
int depth = 0,
HashSet<string>? visited = null)
{
if (depth >= MaxDepth) return content; // 최대 깊이 초과
visited ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// @로 시작하는 줄 감지: "@./shared/common.md", "@../global/rules.md"
var regex = new Regex(@"^@(.+)$", RegexOptions.Multiline);
// ... 파일 읽기·재귀·순환 감지 ...
}
// 파일 크기 40,000자 초과 경고
public bool IsOversized(string content) => content.Length > MaxFileSize;
// 순환 참조 감지: visited 집합으로 추적
private void DetectCircular(string filePath, HashSet<string> visited)
{
if (visited.Contains(filePath))
throw new InvalidOperationException($"순환 참조 감지: {filePath}");
visited.Add(filePath);
}
}
17-E-2: PathScopedRuleInjector
// .ax/rules/*.md의 paths: 프론트매터를 읽어 현재 파일에 맞는 규칙만 주입
public class PathScopedRuleInjector
{
private readonly string _rulesDir; // 프로젝트/.ax/rules/
private IReadOnlyList<RuleFile>? _cachedRules;
// 캐시 로드 (FileSystemWatcher로 변경 시 무효화)
public async Task LoadRulesAsync() { ... }
// 현재 파일 경로에 맞는 규칙 파일 필터링
public IReadOnlyList<RuleFile> GetActiveRulesForFile(string currentFilePath)
{
return (_cachedRules ?? Array.Empty<RuleFile>())
.Where(r => r.Frontmatter.Paths.Count == 0 || // paths 없으면 항상 적용
r.Frontmatter.Paths.Any(p => GlobMatcher.IsMatch(currentFilePath, p)))
.ToList();
}
// 시스템 프롬프트 주입용 텍스트 빌드
public string BuildInjection(IReadOnlyList<RuleFile> rules)
{
var sb = new StringBuilder("## 프로젝트 규칙\n\n");
foreach (var rule in rules)
sb.AppendLine(rule.Body).AppendLine();
return sb.ToString();
}
}
public record RuleFile
{
public string FilePath { get; init; }
public RuleFrontmatter Frontmatter { get; init; }
public string Body { get; init; }
}
public record RuleFrontmatter
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("paths")]
public IReadOnlyList<string> Paths { get; init; } = Array.Empty<string>();
// 빈 배열 → 항상 적용
// ["src/**/*.py"] → Python 파일 작업 시만 적용
}
17-E-3: ContextCompactionService + /compact 명령
// 컨텍스트 압축 서비스
public class ContextCompactionService
{
private readonly AgentLoopService _agentLoop;
private readonly HookRunnerService _hookRunner;
private readonly TaskStateService _taskState;
// /compact 슬래시 명령 처리
public async Task<CompactionResult> CompactAsync(
string sessionId,
CompactionOptions opts,
CancellationToken ct)
{
// 1. PreCompact 훅 실행
await _hookRunner.RunAsync(HookEvent.PreCompact, ..., ct);
// 2. 현재 TaskState를 새 컨텍스트 요약에 포함
var stateContext = _taskState.BuildCompactContextInjection();
// 3. LLM으로 대화 이력 요약 생성
var summary = await GenerateSummaryAsync(sessionId, stateContext, ct);
// 4. 대화 이력 교체 (요약으로)
_agentLoop.ReplaceHistoryWithSummary(sessionId, summary);
// 5. PostCompact 훅 실행
await _hookRunner.RunAsync(HookEvent.PostCompact, ..., ct);
return new CompactionResult { Summary = summary, SavedTokens = ... };
}
// rewind_files: 특정 메시지 이후 에이전트가 수정한 파일 되돌리기
public async Task RewindFilesAsync(string sessionId, string afterMessageId)
{
// 1. AgentEventLog에서 afterMessageId 이후 file_edit 이벤트 수집
// 2. 각 이벤트의 이전 파일 내용(백업)을 복원
// 3. git checkout -- <files> 또는 내부 백업에서 복원
}
}
public record CompactionOptions
{
public bool KeepTaskState { get; init; } = true;
public bool KeepRecentN { get; init; } = true;
public int RecentNMessages { get; init; } = 5; // 최근 N개 메시지는 유지
}
17-E-4: /compact 슬래시 명령 등록
// SlashCommandRegistry 에 등록
public class CompactSlashCommand : ISlashCommand
{
public string Name => "compact";
public string Description => "대화 컨텍스트를 수동으로 압축합니다";
public bool UserInvocable => true;
private readonly ContextCompactionService _compaction;
public async Task<SlashCommandResult> ExecuteAsync(
string args, AgentSession session, CancellationToken ct)
{
var result = await _compaction.CompactAsync(session.Id, new CompactionOptions(), ct);
return SlashCommandResult.Success(
$"컨텍스트 압축 완료. 절약된 토큰: {result.SavedTokens:N0}");
}
}
17-E-5: 파일 크기 경고 UI
// WorkflowAnalyzerViewModel 또는 AgentSessionHeaderViewModel 에 추가
public bool HasOversizedRules { get; set; } // 40,000자 초과 시 true
public string OversizedRuleWarning { get; set; } // "python.md가 40,000자를 초과합니다. 파일 분리를 권장합니다."
// AgentSessionHeaderBar.xaml에 경고 배지 표시
// <Border Visibility="{Binding HasOversizedRules, ...}" Background="#EF4444" ...>
// <TextBlock Text="규칙 크기 경고" FontSize="11"/>
// </Border>
Phase 17-F — 권한 시스템 고도화 (v1.8.0)
목표: acceptEdits 모드 추가 + 패턴 기반 허용/차단 규칙 + MCP 도구 권한
17-F-1: PermissionMode 열거형 확장
// 기존 AgentDecisionLevel → PermissionMode 로 개념 통합
public enum PermissionMode
{
Default, // 기존: 잠재적으로 위험한 작업에 확인 요청
AcceptEdits, // 신규: 파일 편집 자동승인, bash/process 확인 유지
Plan, // 기존: 읽기 전용, 쓰기 차단
BypassPermissions // 기존: 모든 확인 건너뜀 (자동화 전용)
}
17-F-2: PermissionRule + Chain (Chain of Responsibility 패턴)
// 권한 규칙 하나
public record PermissionRule
{
[JsonPropertyName("tool")]
public string ToolName { get; init; } // "process", "file_edit", "mcp__myserver__*"
[JsonPropertyName("pattern")]
public string? Pattern { get; init; } // "git *", "rm -rf *", null(모든 입력)
[JsonPropertyName("behavior")]
public PermissionBehavior Behavior { get; init; } // Allow | Deny | Ask
}
public enum PermissionBehavior { Allow, Deny, Ask }
// 책임 연쇄 핸들러 인터페이스
public abstract class PermissionHandler
{
protected PermissionHandler? _next;
public PermissionHandler SetNext(PermissionHandler next)
{
_next = next;
return next;
}
public abstract PermissionDecision? Handle(string toolName, string input);
}
// 구현: Deny 규칙 핸들러
public class DenyRuleHandler : PermissionHandler
{
private readonly IReadOnlyList<PermissionRule> _denyRules;
public override PermissionDecision? Handle(string toolName, string input)
{
if (_denyRules.Any(r => Matches(r, toolName, input)))
return PermissionDecision.Deny;
return _next?.Handle(toolName, input);
}
private bool Matches(PermissionRule rule, string toolName, string input)
=> rule.ToolName == toolName &&
(rule.Pattern == null || GlobMatcher.IsMatch(input, rule.Pattern));
}
// 구현: Allow 규칙 핸들러
public class AllowRuleHandler : PermissionHandler
{
private readonly IReadOnlyList<PermissionRule> _allowRules;
public override PermissionDecision? Handle(string toolName, string input) { ... }
}
// 구현: AcceptEdits 모드 핸들러
public class AcceptEditsHandler : PermissionHandler
{
private readonly PermissionMode _mode;
private static readonly HashSet<string> _editTools = new() { "file_edit", "file_write", "script_create" };
public override PermissionDecision? Handle(string toolName, string input)
{
if (_mode == PermissionMode.AcceptEdits && _editTools.Contains(toolName))
return PermissionDecision.Allow; // 파일 편집 자동승인
return _next?.Handle(toolName, input);
}
}
// 권한 결정 서비스 (Chain 조립)
public class PermissionDecisionService
{
private readonly PermissionHandler _chain;
public PermissionDecisionService(AppSettings settings)
{
// Chain 조립: Deny → Allow → AcceptEdits → Default(Ask)
var denyHandler = new DenyRuleHandler(settings.Permissions.DenyRules);
var allowHandler = new AllowRuleHandler(settings.Permissions.AllowRules);
var acceptEditsHandler = new AcceptEditsHandler(settings.Permissions.Mode);
var defaultHandler = new DefaultAskHandler();
denyHandler.SetNext(allowHandler).SetNext(acceptEditsHandler).SetNext(defaultHandler);
_chain = denyHandler;
}
public PermissionDecision Decide(string toolName, string input)
=> _chain.Handle(toolName, input) ?? PermissionDecision.Ask;
}
17-F-3: MCP 도구 권한
// MCP 도구 규칙: mcp__서버명, mcp__서버명__도구명
// ToolName 패턴: "mcp__myserver", "mcp__myserver__query_db"
// PermissionRule에서 ToolName="mcp__myserver" → 해당 서버 모든 도구 차단
public class McpPermissionFilter
{
private readonly PermissionDecisionService _permissions;
// McpClientService.ExecuteToolAsync() 호출 전 적용
public bool IsToolAllowed(string serverName, string toolName)
{
var fullName = $"mcp__{serverName}__{toolName}";
var serverPattern = $"mcp__{serverName}";
var byTool = _permissions.Decide(fullName, string.Empty);
if (byTool == PermissionDecision.Deny) return false;
var byServer = _permissions.Decide(serverPattern, string.Empty);
if (byServer == PermissionDecision.Deny) return false;
return true;
}
}
17-F-4: 설정 스키마 변경
// AppSettings.cs 에 추가
public class AppSettings
{
[JsonPropertyName("permissions")]
public PermissionsConfig Permissions { get; set; } = new();
}
public class PermissionsConfig
{
[JsonPropertyName("mode")]
public PermissionMode Mode { get; set; } = PermissionMode.Default;
[JsonPropertyName("allow")]
public List<PermissionRule> AllowRules { get; set; } = new();
// 예: [{"tool":"process","pattern":"git *","behavior":"allow"}]
[JsonPropertyName("deny")]
public List<PermissionRule> DenyRules { get; set; } = new();
// 예: [{"tool":"process","pattern":"rm -rf *","behavior":"deny"}]
}
17-F-5: 권한 편집기 UI (SettingsWindow — 전문 설정 탭)
[설정창 > 권한 탭]
권한 모드: [Default ▾] (Default / AcceptEdits / Plan / BypassPermissions)
── 허용 규칙 ──────────────────────────────
[도구: process ] [패턴: git * ] [허용 ▾] [삭제]
[도구: process ] [패턴: dotnet * ] [허용 ▾] [삭제]
[+ 규칙 추가]
── 차단 규칙 ──────────────────────────────
[도구: process ] [패턴: rm -rf * ] [차단 ▾] [삭제]
[도구: mcp__외부서버 ] [패턴: (전체) ] [차단 ▾] [삭제]
[+ 규칙 추가]
// PermissionsTabViewModel (SettingsWindow 내 탭)
public class PermissionsTabViewModel : ViewModelBase
{
public PermissionMode SelectedMode { get; set; }
public ObservableCollection<PermissionRuleViewModel> AllowRules { get; }
public ObservableCollection<PermissionRuleViewModel> DenyRules { get; }
public ICommand AddAllowRuleCommand { get; }
public ICommand AddDenyRuleCommand { get; }
public ICommand DeleteRuleCommand { get; } // param: PermissionRuleViewModel
public ICommand SaveCommand { get; }
}
17-F-6: MCP HTTP+SSE 전송 추가
// McpClientService.cs 에 전송 타입 추가
public enum McpTransport { Stdio, Http, Sse }
public class McpServerConfig
{
// 기존
[JsonPropertyName("command")]
public string? Command { get; init; }
[JsonPropertyName("args")]
public IReadOnlyList<string> Args { get; init; } = Array.Empty<string>();
// 신규
[JsonPropertyName("type")]
public McpTransport Transport { get; init; } = McpTransport.Stdio;
[JsonPropertyName("url")]
public string? Url { get; init; } // HTTP/SSE 전용
[JsonPropertyName("headers")]
public Dictionary<string, string> Headers { get; init; } = new();
// $VAR 환경변수 치환 지원
}
// McpClientService 에 HTTP/SSE 클라이언트 추가
public class McpClientService
{
// 기존: stdio 클라이언트
// 신규: HTTP 전송 클라이언트
private async Task<McpResponse> SendHttpAsync(McpServerConfig config, McpRequest req, CancellationToken ct)
{
// headers의 $VAR → 환경변수 치환
// POST {config.Url} + Authorization 헤더
}
// 신규: SSE 전송 클라이언트
private async Task<McpResponse> SubscribeSseAsync(McpServerConfig config, McpRequest req, CancellationToken ct)
{
// Server-Sent Events 수신
}
}
Phase 17-G — 멀티파일 Diff + 자동 컨텍스트 수집 (v1.8.0)
17-G-1: 멀티파일 통합 Diff 뷰
// 기존 DiffPanel 확장
public class MultiFileDiffViewModel : ViewModelBase
{
public ObservableCollection<FileDiffViewModel> FileDiffs { get; }
public int TotalChangedFiles => FileDiffs.Count;
public int TotalAddedLines { get; }
public int TotalRemovedLines { get; }
// 전체 수락/거부
public ICommand AcceptAllCommand { get; }
public ICommand RejectAllCommand { get; }
// 파일별 수락/거부
public ICommand AcceptFileCommand { get; } // param: FileDiffViewModel
public ICommand RejectFileCommand { get; } // param: FileDiffViewModel
}
public class FileDiffViewModel : ViewModelBase
{
public string FilePath { get; }
public ObservableCollection<DiffHunkViewModel> Hunks { get; }
public bool IsExpanded { get; set; } = true; // 파일별 접기/펼치기
public DiffFileStatus Status { get; } // Modified | Added | Deleted | Renamed
public ICommand AcceptCommand { get; }
public ICommand RejectCommand { get; }
}
XAML 핵심:
<!-- AgentDiffPanel.xaml -->
<Grid>
<!-- 상단: 변경 요약 + 전체 수락/거부 -->
<Border Grid.Row="0" Height="40">
<DockPanel>
<TextBlock Text="{Binding TotalChangedFiles, StringFormat='{}{0}개 파일 변경'}"
Foreground="{DynamicResource PrimaryText}"/>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Border Style="{StaticResource AcceptButtonStyle}"
MouseLeftButtonUp="...">
<TextBlock Text="전체 수락" Foreground="#22C55E"/>
</Border>
<Border Style="{StaticResource RejectButtonStyle}"
MouseLeftButtonUp="...">
<TextBlock Text="전체 거부" Foreground="#EF4444"/>
</Border>
</StackPanel>
</DockPanel>
</Border>
<!-- 파일별 diff -->
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding FileDiffs}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:FileDiffViewModel}">
<!-- 파일 헤더: 경로 + 수락/거부 -->
<!-- Hunk 목록 -->
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
17-G-2: 자동 컨텍스트 수집
// 사용자 메시지에서 파일명 감지 → 자동 읽기
public class AutoContextCollector
{
private readonly IAgentTool _fileReadTool;
// 패턴: "파일명.확장자", "`백틱경로`", "src/path/to/file" 형태 감지
private static readonly Regex _filePatterns = new(
@"(?:^|\s)([A-Za-z가-힣_\-\.\/\\]+\.[a-zA-Z]{1,6})(?:\s|$)|`([^`]+)`",
RegexOptions.Multiline);
public async Task<IReadOnlyList<FileContext>> CollectAsync(
string userMessage, string projectRoot, CancellationToken ct)
{
var matches = _filePatterns.Matches(userMessage);
var results = new List<FileContext>();
foreach (Match m in matches)
{
var path = m.Groups[1].Value ?? m.Groups[2].Value;
var fullPath = Path.Combine(projectRoot, path);
if (File.Exists(fullPath))
{
var content = await File.ReadAllTextAsync(fullPath, ct);
results.Add(new FileContext { Path = path, Content = content });
}
}
return results;
}
}
// 도구 위험도 정적 매핑
public static class ToolRiskMapper
{
public static ToolRisk GetRisk(string toolName) => toolName switch
{
"file_read" or "code_search" or "list_directory" => ToolRisk.Low,
"file_edit" or "script_create" or "git_commit" => ToolRisk.Medium,
"process" or "bash" or "file_delete" or "registry_write" => ToolRisk.High,
_ when toolName.StartsWith("mcp__") => ToolRisk.Medium,
_ => ToolRisk.Medium
};
}
public enum ToolRisk { Low, Medium, High }
Phase 18-A — 멀티에이전트 팀 + Worktree 격리 (v2.0)
목표: 코디네이터 에이전트 모드·Worktree 격리·위임 도구·백그라운드 에이전트
18-A-1: 코디네이터 에이전트
// 코디네이터 모드: 계획/라우팅 전담
public interface IAgentCoordinator
{
Task<CoordinatorPlan> CreatePlanAsync(string userRequest, CancellationToken ct);
Task<string> ExecutePlanAsync(CoordinatorPlan plan, IProgress<PlanProgress> progress, CancellationToken ct);
}
public class CoordinatorAgent : IAgentCoordinator
{
private readonly AgentLoopService _loop;
private readonly DelegateAgentTool _delegateTool;
public async Task<CoordinatorPlan> CreatePlanAsync(string userRequest, CancellationToken ct)
{
// 전용 시스템 프롬프트로 계획 생성
// 출력: JSON 형태의 SubtaskList
// 각 서브태스크: agentType, description, dependencies[]
}
public async Task<string> ExecutePlanAsync(CoordinatorPlan plan, IProgress<PlanProgress> progress, CancellationToken ct)
{
// 의존성 없는 서브태스크 병렬 실행
// 완료된 서브태스크 결과를 다음 서브태스크 컨텍스트에 주입
// 최종 결과 병합
}
}
public record CoordinatorPlan
{
public string OriginalRequest { get; init; }
public IReadOnlyList<SubTask> Tasks { get; init; }
}
public record SubTask
{
public string Id { get; init; }
public string AgentType { get; init; } // "code-reviewer", "researcher", "implementer"
public string Description { get; init; }
public IReadOnlyList<string> Dependencies { get; init; } // 선행 SubTask Id 목록
public SubTaskStatus Status { get; set; } // Pending | Running | Completed | Failed
public string? Result { get; set; }
}
public enum SubTaskStatus { Pending, Running, Completed, Failed }
18-A-2: WorktreeManager
// git worktree 기반 격리 실행 환경
public class WorktreeManager : IDisposable
{
private readonly List<Worktree> _active = new();
// 새 worktree 생성 (임시 브랜치)
public async Task<Worktree> CreateAsync(
string repoRoot,
string branchName,
CancellationToken ct)
{
var worktreePath = Path.Combine(Path.GetTempPath(), "ax-worktree-" + Guid.NewGuid().ToString("N")[..8]);
// git worktree add <path> -b <branch>
await RunGitAsync($"worktree add {worktreePath} -b {branchName}", repoRoot, ct);
var wt = new Worktree { Path = worktreePath, Branch = branchName, RepoRoot = repoRoot };
_active.Add(wt);
return wt;
}
// 변경 사항 병합 (승인 시)
public async Task MergeAsync(Worktree worktree, string targetBranch, CancellationToken ct)
{
await RunGitAsync($"merge {worktree.Branch}", worktree.RepoRoot, ct);
}
// worktree 제거 (거부 시)
public async Task DisposeAsync(Worktree worktree, bool keepBranch = false, CancellationToken ct = default)
{
await RunGitAsync($"worktree remove {worktree.Path} --force", worktree.RepoRoot, ct);
if (!keepBranch)
await RunGitAsync($"branch -D {worktree.Branch}", worktree.RepoRoot, ct);
_active.Remove(worktree);
}
private Task RunGitAsync(string args, string cwd, CancellationToken ct) { ... }
public void Dispose() { /* 남은 worktree 정리 */ }
}
public record Worktree
{
public string Path { get; init; }
public string Branch { get; init; }
public string RepoRoot { get; init; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}
18-A-3: DelegateAgentTool
// IAgentTool 구현: 전문 서브에이전트에 작업 위임
public class DelegateAgentTool : IAgentTool
{
public string Name => "delegate";
public string Description => "전문 에이전트에 작업을 위임합니다";
public JsonObject Parameters => new()
{
["type"] = "object",
["properties"] = new JsonObject
{
["agent_type"] = new JsonObject
{
["type"] = "string",
["enum"] = new JsonArray("researcher", "code-reviewer", "implementer", "security-auditor", "doc-writer"),
["description"] = "위임할 에이전트 유형"
},
["task"] = new JsonObject
{
["type"] = "string",
["description"] = "에이전트에게 전달할 작업 설명"
},
["context"] = new JsonObject
{
["type"] = "string",
["description"] = "에이전트에게 전달할 추가 컨텍스트"
},
["isolation"] = new JsonObject
{
["type"] = "string",
["enum"] = new JsonArray("none", "worktree"),
["description"] = "격리 수준: worktree → git worktree 격리"
}
},
["required"] = new JsonArray("agent_type", "task")
};
private readonly BackgroundAgentService _backgroundAgents;
private readonly WorktreeManager _worktreeManager;
private readonly AgentTypeMemoryRepository _agentMemory;
public async Task<ToolResult> ExecuteAsync(JsonObject args, CancellationToken ct)
{
var agentType = args["agent_type"]!.GetValue<string>();
var task = args["task"]!.GetValue<string>();
var context = args["context"]?.GetValue<string>() ?? string.Empty;
var isolation = args["isolation"]?.GetValue<string>() ?? "none";
// 에이전트 타입별 메모리 로드
var memory = await _agentMemory.LoadMemoryAsync(agentType);
// worktree 격리 필요 시 생성
Worktree? worktree = null;
if (isolation == "worktree")
worktree = await _worktreeManager.CreateAsync(
GetCurrentRepoRoot(), $"ax-{agentType}-{DateTime.UtcNow:yyMMddHHmm}", ct);
// 서브에이전트 실행
var result = await _backgroundAgents.StartAsync(new AgentTask
{
AgentType = agentType,
Description = task,
Context = context + (memory != null ? $"\n\n## 이전 학습:\n{memory}" : string.Empty),
WorkingDirectory = worktree?.Path ?? GetCurrentDirectory()
}, ct);
// 결과 크기 제한: 100,000자
if (result.Length > 100_000)
result = result[..100_000] + "\n\n[결과가 100,000자를 초과하여 잘림]";
return ToolResult.Ok(result);
}
}
18-A-4: BackgroundAgentService
// 비동기 서브에이전트 실행 + 완료 알림
public class BackgroundAgentService
{
private readonly ConcurrentDictionary<string, BackgroundAgentTask> _running = new();
// 이벤트: 완료 시 발화 (트레이 알림 연결)
public event EventHandler<AgentCompletedEventArgs>? AgentCompleted;
// 비동기 실행 시작 (즉시 반환, 완료 시 이벤트)
public async Task<string> StartAsync(AgentTask task, CancellationToken ct)
{
var id = Guid.NewGuid().ToString("N")[..8];
var bgTask = new BackgroundAgentTask { Id = id, Task = task, StartedAt = DateTime.UtcNow };
_running[id] = bgTask;
_ = Task.Run(async () =>
{
try
{
// 격리된 AgentLoopService 인스턴스 생성
var result = await RunSubAgentLoopAsync(task, ct);
bgTask.Result = result;
bgTask.Status = BackgroundAgentStatus.Completed;
AgentCompleted?.Invoke(this, new AgentCompletedEventArgs { Task = bgTask, Result = result });
}
catch (Exception ex)
{
bgTask.Status = BackgroundAgentStatus.Failed;
bgTask.Error = ex.Message;
AgentCompleted?.Invoke(this, new AgentCompletedEventArgs { Task = bgTask, Error = ex.Message });
}
finally { _running.TryRemove(id, out _); }
}, ct);
return id;
}
public IReadOnlyList<BackgroundAgentTask> GetActive()
=> _running.Values.ToList();
}
// App.xaml.cs 에서 트레이 알림 연결
// _backgroundAgents.AgentCompleted += (s, e) =>
// _trayIcon.ShowBalloonTip(e.Task.AgentType + " 완료", e.Result?.Length > 100 ? e.Result[..100] + "..." : e.Result, ToolTipIcon.Info);
18-A-5: AgentTypeMemoryRepository
// 에이전트 타입별 영속 메모리
// 저장: %APPDATA%\AxCopilot\agent-memory\<agentType>\MEMORY.md
public class AgentTypeMemoryRepository
{
private readonly string _baseDir; // %APPDATA%\AxCopilot\agent-memory\
public async Task<string?> LoadMemoryAsync(string agentType)
{
var path = GetPath(agentType);
return File.Exists(path) ? await File.ReadAllTextAsync(path) : null;
}
public async Task SaveMemoryAsync(string agentType, string content)
{
Directory.CreateDirectory(Path.GetDirectoryName(GetPath(agentType))!);
await File.WriteAllTextAsync(GetPath(agentType), content);
}
// 학습 내용 추가 (기존 내용 끝에 append)
public async Task AppendLearnAsync(string agentType, string learning)
{
var existing = await LoadMemoryAsync(agentType) ?? string.Empty;
var entry = $"\n- [{DateTime.Now:yyyy-MM-dd HH:mm}] {learning}";
await SaveMemoryAsync(agentType, existing + entry);
}
private string GetPath(string agentType)
=> Path.Combine(_baseDir, agentType, "MEMORY.md");
}
18-A-6: 코디네이터 모드 UI
// AgentSessionHeaderViewModel 에 추가
public bool IsCoordinatorMode { get; set; }
public ICommand ToggleCoordinatorModeCommand { get; }
// AgentSessionHeaderBar.xaml 에 추가:
// [코디네이터 모드 토글 버튼] — 활성 시 AccentColor로 강조
// 코디네이터 모드 ON 시: 서브태스크 진행 패널 표시
// CoordinatorPlanViewModel: SubTask 목록 + 진행 상태 표시
public class CoordinatorPlanViewModel : ViewModelBase
{
public ObservableCollection<SubTaskViewModel> Tasks { get; }
public int CompletedCount { get; }
public int TotalCount { get; }
public double Progress => TotalCount > 0 ? (double)CompletedCount / TotalCount : 0;
}
Phase 18-B — 에이전트 리플레이/디버깅 (v2.0)
목표: Phase 17-B의 이벤트 로그를 활용한 세션 재생 및 분기 재실행
// 세션 리플레이 서비스
public class AgentReplayService
{
private readonly AgentEventLog _eventLog;
private readonly AgentLoopService _agentLoop;
// 특정 시점까지 이벤트 재생
public async Task ReplayToAsync(string sessionId, long upToSeqNo, CancellationToken ct)
{
await foreach (var evt in _eventLog.ReadFromAsync(0).WithCancellation(ct))
{
if (evt.SeqNo > upToSeqNo) break;
await ApplyEventAsync(evt);
}
}
// 특정 시점에서 분기 (새 세션 생성)
public async Task<string> ForkFromAsync(string sessionId, long forkAtSeqNo, CancellationToken ct)
{
var newSessionId = Guid.NewGuid().ToString("N");
await ReplayToAsync(sessionId, forkAtSeqNo, ct);
// 새 세션 ID로 계속
return newSessionId;
}
// ViewModel: 타임라인 UI
// WorkflowAnalyzerViewModel 에 리플레이 타임라인 패널 추가
}
public class ReplayTimelineViewModel : ViewModelBase
{
public ObservableCollection<TimelineEventViewModel> Events { get; }
public long CurrentPosition { get; set; }
public ICommand SeekToCommand { get; } // param: long seqNo
public ICommand ForkFromCommand { get; } // param: long seqNo
public ICommand PlayPauseCommand { get; }
}
Phase 18-C — 플러그인 갤러리 + 생태계 확장 (v2.0)
18-C-1: 플러그인 갤러리
// 플러그인 매니페스트
public record PluginManifest
{
[JsonPropertyName("id")]
public string Id { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; }
[JsonPropertyName("version")]
public string Version { get; init; }
[JsonPropertyName("description")]
public string Description { get; init; }
[JsonPropertyName("type")]
public PluginType Type { get; init; } // Skill | Tool | Theme
[JsonPropertyName("entry")]
public string EntryFile { get; init; } // zip 내부 진입점 파일
[JsonPropertyName("min_app_version")]
public string MinAppVersion { get; init; }
}
// 플러그인 설치 서비스
public class PluginInstallService
{
// zip 파일에서 설치
public async Task<InstallResult> InstallFromZipAsync(string zipPath, CancellationToken ct)
{
// 1. zip 압축 해제 → 임시 디렉토리
// 2. manifest.json 파싱·검증
// 3. %APPDATA%\AxCopilot\plugins\<id>\ 에 복사
// 4. 플러그인 타입에 따라 등록:
// - Skill → SkillLoaderService.Reload()
// - Tool → ToolRegistry.Register()
}
// 로컬 레지스트리 갱신 (NAS/Git 기반)
public async Task RefreshRegistryAsync(string registryPath, CancellationToken ct) { ... }
// 설치된 플러그인 목록
public IReadOnlyList<InstalledPlugin> GetInstalled() { ... }
// 플러그인 제거
public Task UninstallAsync(string pluginId, CancellationToken ct) { ... }
}
Phase L3 — 런처 에코시스템 (v2.0)
| # | 기능 | 연결 Phase | 핵심 클래스 |
|---|---|---|---|
| L3-1 | 플러그인 갤러리 | 18-C1 | PluginGalleryViewModel, PluginInstallService |
| L3-2 | 웹 검색 AI 요약 | 18-C5 | WebSearchSummaryHandler, ContentExtractor |
| L3-3 | AI 스니펫 | 18-C2 | AiSnippetHandler, SnippetTemplateService |
| L3-4 | 파라미터 퀵링크 | 18-C3 | QuickLinkHandler, UrlTemplateEngine |
| L3-7 | 알림 센터 통합 | 18-A4 | NotificationCenterService (Windows Toast API) |
구현 의존성 그래프
Phase 17 구현 순서:
[17-UI-1~4] AgentWindow 골격 + ViewModel
↓
[17-UI-5~6] Sidebar + InlineSettings (SettingsWindow 이전)
↓
[17-B-1~2] TaskState + EventLog (인프라 먼저)
↓
[17-C-1~5] Hook 이벤트/타입 확장 (TaskState 이용)
↓
[17-D-1~5] Skill fork/paths (Hook 인프라 이용)
↓
[17-E-1~4] @include + 경로 규칙 (Skill 시스템과 병렬)
↓
[17-F-1~5] Permission Chain + acceptEdits
↓
[17-A-1~4] Reflexion (모든 인프라 완성 후)
↓
[17-G-1~2] MultiFileDiff + AutoContext (독립, 병렬 가능)
Phase 18 구현 순서:
[18-A-2] WorktreeManager (git 인프라)
↓
[18-A-4] BackgroundAgentService
↓
[18-A-3] DelegateAgentTool (WorktreeManager + BackgroundAgent 이용)
↓
[18-A-1] CoordinatorAgent (DelegateTool 이용)
↓
[18-A-5] AgentTypeMemoryRepository
↓
[18-B] ReplayService (EventLog Phase 17-B 이용)
↓
[18-C] 플러그인 갤러리 (독립)
버전별 출시 계획
| 버전 | 코드명 | 포함 Phase | 핵심 신규 클래스 수 |
|---|---|---|---|
| v1.8.0 | 에이전트 인프라 | 17-UI, 17-A~G | ~35개 신규 클래스 ✅ 완료 |
| v1.8.1 | 인프라 안정화 | 버그픽스 + 17-* 마이너 | — |
| v2.0 | 에이전트 팀 | 18-A~C, L3 | ~25개 신규 클래스 ✅ 완료 |
| v2.1 | CC 동등성 | 19-A~G (Claude Code 갭 해소) | ~30개 신규 클래스 |
| v2.2 | SDK 프로토콜 | 20-A~C (임베딩 + 외부 연동) | ~15개 신규 클래스 |
| v2.3 | AX Agent UI 전면 개편 | 21 (Claude.ai+Codex 레이아웃) | UI 계층 60% 재작성 |
| v3.0 | 크로스플랫폼 | LP-1~3 (Avalonia) | UI 계층 40% 재작성 |
Phase 19 — Claude Code 동등성 달성 (v2.1)
목표:
docs/claude-code-docs-main/문서 분석 결과 발견된 AX Agent 갭을 해소하여 Claude Code 수준의 에이전트 인프라를 달성한다.
갭 분석 요약
| 기능 영역 | Claude Code | AX Agent 현황 | 우선순위 Phase |
|---|---|---|---|
| 계층형 메모리 (4층) | 시스템/유저/프로젝트/로컬 + @include | AX.md 단일 파일 | 19-A |
| 권한 패턴 매칭 | git *, npm run * 패턴 + 복합 명령 분해 |
경로 기반 Allow/Deny만 | 19-B |
| 훅 이벤트 완성 | 14+ 이벤트, Shell/HTTP/LLM/Agent 타입 | 일부 이벤트, Shell 위주 | 19-C |
| 스킬 인라인 실행 | !``cmd`` 실행 치환, 유저급 스킬, 네임스페이스 |
프리셋 파일 치환 없음 | 19-D |
| 세션 재개/포크 | --resume, forkSession(), 태깅 |
JSONL 리플레이만 | 19-E |
| 출력 예산 관리 | maxResultSizeChars, 임시파일 스필오버 |
고정 8000자 절단 | 19-F |
| /init 명령 | 프로젝트 분석 → AX.md 자동 생성 | 없음 | 19-G |
Phase 19-A — 계층형 메모리 시스템 (v2.1)
목표: AX.md 단일 파일 → 4계층 캐스케이드 메모리 시스템. 경로 기반 규칙 파일 지원.
4계층 메모리 아키텍처
| 계층 | 경로 | 범위 | 우선순위 |
|---|---|---|---|
| L1 시스템 | %APPDATA%\AxCopilot\system.md |
앱 전역 관리자 규칙 | 최하위 |
| L2 유저 | %USERPROFILE%\.axcopilot\AX.md |
유저 전체 선호 | ↑ |
| L3 프로젝트 | <cwd>\AX.md + <cwd>\.axcopilot\rules\*.md |
팀 공유 규칙 | ↑ |
| L4 로컬 | <cwd>\AX.local.md |
개인 프로젝트 오버라이드 (gitignore) | 최상위 |
핵심 클래스
// HierarchicalMemoryLoader.cs
public class HierarchicalMemoryLoader
{
/// <summary>CWD에서 루트까지 탐색하며 계층별 메모리 파일 수집.</summary>
public async Task<MemoryContext> LoadAsync(string workFolder, CancellationToken ct)
{
var layers = new List<MemoryLayer>();
layers.Add(await LoadSystemLayerAsync(ct)); // L1
layers.Add(await LoadUserLayerAsync(ct)); // L2
layers.AddRange(await LoadProjectLayersAsync(workFolder, ct)); // L3 (CWD→루트)
layers.Add(await LoadLocalLayerAsync(workFolder, ct)); // L4
return new MemoryContext(layers);
}
/// <summary>@include 지시문 처리 (최대 5단계 중첩, 순환 참조 감지).</summary>
private async Task<string> ResolveIncludesAsync(string content, string basePath,
HashSet<string> visited, int depth, CancellationToken ct) { ... }
}
// PathScopedRuleLoader.cs — .axcopilot/rules/*.md 파일 로드
public class PathScopedRuleLoader
{
/// <summary>현재 편집 파일 경로와 rules/*.md의 paths 패턴을 매칭하여 주입할 규칙 반환.</summary>
public IReadOnlyList<string> GetApplicableRules(string editingFilePath,
IReadOnlyList<RuleFile> ruleFiles) { ... }
}
// MemoryContext.cs
public record MemoryLayer(string Source, string Content, MemoryScope Scope);
public enum MemoryScope { System, User, Project, Local }
public record MemoryContext(IReadOnlyList<MemoryLayer> Layers)
{
/// <summary>모든 레이어를 우선순위 순서로 조합한 최종 컨텍스트 문자열.</summary>
public string Compose() => string.Join("\n\n", Layers.Select(l => l.Content));
}
AgentLoopService 통합
// AgentLoopService.cs — 컨텍스트 조립 부분 교체
private async Task<string> BuildSystemContextAsync(CancellationToken ct)
{
// 기존: 단일 AX.md 로드
// 변경: 계층형 로더 사용
var memCtx = await _memoryLoader.LoadAsync(_workFolder, ct);
var pathRules = _ruleScopeLoader.GetApplicableRules(_currentEditingFile, _cachedRules);
return memCtx.Compose() + "\n\n" + string.Join("\n\n", pathRules);
}
슬래시 명령 추가
// /memory 슬래시 명령 → ChatWindow에서 메모리 파일 인라인 에디터 열기
// 현재 로드된 모든 메모리 레이어 표시 + 편집 버튼
public class MemorySlashCommand : ISlashCommand
{
public string Name => "memory";
public string Description => "로드된 메모리 파일 목록 및 편집";
public Task ExecuteAsync(string args, IChatContext ctx);
}
설정 항목
// AppSettings.LlmSettings
public bool EnableHierarchicalMemory { get; set; } = true;
public string UserMemoryPath { get; set; } = ""; // 기본: %USERPROFILE%\.axcopilot\AX.md
public int MaxIncludeDepth { get; set; } = 5;
Phase 19-B — 권한 패턴 매칭 + 복합 명령 분해 (v2.1)
목표: 현재 경로 기반 Allow/Deny → 명령 패턴 매칭 +
&&/||/;/|복합 명령 서브 커맨드별 검증.
핵심 클래스
// CommandPatternMatcher.cs
public class CommandPatternMatcher
{
/// <summary>패턴(glob) 기반 명령 허용 여부 검사.
/// 예: "git *" → git commit, git push 모두 허용
/// "npm run *" → npm run build, npm run test 허용
/// "rm -rf *" → 금지 리스트에 명시 가능</summary>
public PermissionDecision Match(string command, IReadOnlyList<PermissionRuleEntry> rules) { ... }
}
// CompoundCommandParser.cs
public class CompoundCommandParser
{
private static readonly string[] Operators = ["&&", "||", ";", "|"];
/// <summary>복합 명령을 분해하여 서브 커맨드 목록 반환.
/// 예: "git add . && git commit -m 'msg'" → ["git add .", "git commit -m 'msg'"]</summary>
public IReadOnlyList<string> Parse(string command) { ... }
/// <summary>복합 명령의 모든 서브 커맨드에 대해 권한 검사.
/// 하나라도 Deny이면 전체 Deny.</summary>
public PermissionDecision CheckAll(string compoundCommand,
CommandPatternMatcher matcher,
IReadOnlyList<PermissionRuleEntry> rules) { ... }
}
// 항상 차단되는 명령 목록 (하드코딩)
public static class AlwaysBlockedCommands
{
public static readonly string[] Patterns =
[
"rd /s", "rmdir /s", // 재귀 폴더 삭제
"format *", // 드라이브 포맷
"reg delete *", // 레지스트리 삭제
"del /f /s /q *", // 강제 재귀 파일 삭제
"netsh * delete *", // 방화벽 규칙 삭제
"schtasks /delete *", // 작업 스케줄러 삭제
];
}
PermissionRuleEntry 확장
// 기존 PermissionRuleEntry에 패턴 지원 추가
public class PermissionRuleEntry
{
[JsonPropertyName("tool")]
public string Tool { get; set; } = ""; // 도구명 또는 "bash:*"
[JsonPropertyName("pattern")]
public string Pattern { get; set; } = "*"; // glob 패턴 (bash 명령에만 적용)
[JsonPropertyName("decision")]
public string Decision { get; set; } = "allow"; // "allow" | "deny"
[JsonPropertyName("reason")]
public string Reason { get; set; } = ""; // 사용자 표시 이유
}
도구 가시성 필터링
// AgentLoopService — LLM 호출 전 비활성 도구를 tools 목록에서 제거
// 현재: 도구 이름은 넘기되 실행 시 차단
// 개선: 아예 스키마 자체를 LLM에 전달하지 않음 → 환각 방지
private IReadOnlyList<IAgentTool> FilterVisibleTools(IReadOnlyList<IAgentTool> allTools)
{
return allTools.Where(t =>
!_settings.DisabledTools.Contains(t.Name) &&
_permissionSystem.IsToolVisible(t.Name)).ToList();
}
Phase 19-C — 훅 이벤트 완성 (v2.1)
목표: 현재 부분 구현된 훅 시스템 → Claude Code의 14+ 이벤트 + 4가지 훅 타입 완성.
추가 훅 이벤트
// HookTypes.cs 확장
public enum AgentHookEvent
{
// 기존
PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd,
UserPromptSubmit, PreCompact, PostCompact,
// 신규 추가
PostToolUseFailure, // 도구 실행 실패 시
SubAgentStart, // 서브에이전트 시작
SubAgentStop, // 서브에이전트 완료
PermissionRequest, // 승인 다이얼로그 직전
PermissionDenied, // 거부 후
Notification, // UI 알림 발생 시
CwdChanged, // 작업 폴더 변경 시
FileChanged, // 감시 파일 변경 시
ConfigChange, // 설정 파일 변경 시
}
훅 타입 완성 (4종)
// ExtendedHookRunner.cs 확장
public enum HookCommandType { Shell, Http, Llm, Agent }
public class HookEntry
{
public AgentHookEvent Event { get; set; }
public string? Matcher { get; set; } // tool_name 또는 notification_type 패턴
public HookCommandType Type { get; set; } = HookCommandType.Shell;
// Shell 훅
public string? Command { get; set; }
public int Timeout { get; set; } = 30;
public bool Async { get; set; } = false;
public bool AsyncRewake { get; set; } = false; // 완료 시 에이전트 재개
// HTTP 훅
public string? Url { get; set; }
public Dictionary<string, string> Headers { get; set; } = new();
// LLM 훅 (검증용 Claude 호출)
public string? LlmPrompt { get; set; }
public string? LlmModel { get; set; }
// Agent 훅 (풀 에이전트 루프)
public string? AgentPrompt { get; set; }
public string[]? AgentTools { get; set; }
}
// Exit code semantics
// 0 = 성공, 계속
// 2 = 차단/주입 (stderr → Claude에게 전달, 동작 차단 또는 루프 계속)
// 기타 = stderr → 사용자에만 표시, 계속
// $AX_ENV_FILE: CwdChanged 훅에서 환경변수 주입
// 예: echo "AX_PROJECT_TYPE=rust" >> $AX_ENV_FILE
PreToolUse 입력 수정 기능
// 훅이 도구 입력을 수정할 수 있도록 허용
public record HookResult
{
public bool Continue { get; init; } = true;
public string? Reason { get; init; }
public string? SystemMessage { get; init; }
public JsonElement? UpdatedInput { get; init; } // PreToolUse에서 도구 파라미터 수정
public bool SuppressOutput { get; init; } = false;
}
Phase 19-D — 스킬 시스템 완성 (v2.1)
목표: 스킬 파일의
!``cmd``인라인 실행, 유저급 스킬, 네임스페이스, 스킬별 모델 오버라이드 완성.
스킬 인라인 명령 실행
// SkillService.cs 확장
// 스킬 파일 내 !`command` 블록을 호출 시점에 실행하고 출력으로 치환
public class SkillInlineCommandProcessor
{
private static readonly Regex InlineCmd = new(@"!\`([^`]+)\`", RegexOptions.Compiled);
/// <summary>스킬 본문의 !`cmd` 블록을 실행하고 결과로 치환합니다.
/// 예: !`git log --oneline -10` → 최근 10개 커밋 목록으로 치환</summary>
public async Task<string> ProcessAsync(string skillContent, string workFolder,
CancellationToken ct)
{
foreach (Match m in InlineCmd.Matches(skillContent))
{
var cmd = m.Groups[1].Value;
var output = await ExecuteCommandAsync(cmd, workFolder, ct);
skillContent = skillContent.Replace(m.Value, output);
}
return skillContent;
}
}
스킬 프론트매터 확장
---
description: 한 줄 설명 (/skills 목록에 표시)
argument-hint: "[파일명] [옵션]" # 자동완성 힌트
allowed-tools: [file_read, grep] # 허용 도구 제한
model: claude-haiku-4-5-20251001 # 스킬별 모델 오버라이드
user-invocable: false # /skills 목록 숨김 (Claude만 사용)
context: fork # 격리 서브에이전트로 실행
paths: "**/*.cs" # 이 파일 편집 시 자동 활성화
hooks: # 스킬 전용 훅
PostToolUse:
- command: "dotnet build"
---
유저급 스킬 경로
%USERPROFILE%\.axcopilot\skills\<skill-name>\SKILL.md # 유저 전역
<cwd>\.axcopilot\skills\<skill-name>\SKILL.md # 프로젝트
<cwd>\.axcopilot\skills\<ns>\<skill-name>\SKILL.md # 네임스페이스 (/<ns>:<skill>)
스킬 네임스페이스
// /database:migrate → .axcopilot/skills/database/migrate/SKILL.md
// /test:unit → .axcopilot/skills/test/unit/SKILL.md
public class SkillNamespaceResolver
{
public string? ResolveFilePath(string slashCommand, string workFolder) { ... }
// "database:migrate" → "<workFolder>/.axcopilot/skills/database/migrate/SKILL.md"
}
Phase 19-E — 세션 재개 + 포크 + 태깅 (v2.1)
목표: JSONL 리플레이(관찰용) → 실제 세션 재개 + 분기 지점 포크.
세션 관리 API
// AgentSessionManager.cs
public class AgentSessionManager
{
/// <summary>이전 세션을 재개합니다.
/// 메모리 파일은 재개 시점 기준으로 재발견합니다.</summary>
public Task<AgentSession> ResumeAsync(string sessionId, CancellationToken ct) { ... }
/// <summary>현재 세션의 특정 메시지 인덱스 이후를 분기합니다.
/// 원본 세션은 보존됩니다.</summary>
public Task<AgentSession> ForkAsync(string sessionId, int messageIndex, CancellationToken ct) { ... }
/// <summary>세션에 태그를 붙입니다 (즐겨찾기, 분류 등).</summary>
public Task TagAsync(string sessionId, string tag, CancellationToken ct) { ... }
/// <summary>세션 이름을 변경합니다.</summary>
public Task RenameAsync(string sessionId, string newName, CancellationToken ct) { ... }
/// <summary>조건에 맞는 세션 목록 반환.</summary>
public Task<IReadOnlyList<AgentSessionMeta>> ListAsync(
string? tag = null, string? tabType = null, CancellationToken ct = default) { ... }
}
public record AgentSession(string Id, string Name, string TabType, List<ChatMessage> Messages);
public record AgentSessionMeta(string Id, string Name, string TabType,
DateTime CreatedAt, DateTime LastAt, string? Tag, int MessageCount);
에이전트 타입별 영속 메모리 경로
%USERPROFILE%\.axcopilot\agent-memory\<type>\MEMORY.md # 유저 범위
<cwd>\.axcopilot\agent-memory\<type>\MEMORY.md # 프로젝트 범위
<cwd>\.axcopilot\agent-memory-local\<type>\MEMORY.md # 로컬 범위
// AgentTypeMemoryRepository.cs 확장
public class AgentTypeMemoryRepository
{
public enum MemoryScope { User, Project, Local }
public async Task<string?> GetAsync(string agentType, string workFolder,
MemoryScope scope = MemoryScope.Project, CancellationToken ct = default) { ... }
public async Task SaveAsync(string agentType, string workFolder,
string content, MemoryScope scope = MemoryScope.Project,
CancellationToken ct = default) { ... }
}
Phase 19-F — 출력 예산 + 컨텍스트 최적화 (v2.1)
목표: 고정 8000자 절단 → 도구별 출력 예산 + 대형 출력 임시파일 스필오버. 컨텍스트 조립 메모이제이션.
출력 예산 관리
// ToolOutputBudget.cs
public static class ToolOutputBudget
{
// 도구별 기본 최대 출력 크기
private static readonly Dictionary<string, int> DefaultBudgets = new()
{
["file_read"] = 12_000,
["grep"] = 8_000,
["bash"] = 10_000,
["git_tool"] = 6_000,
["http_tool"] = 8_000,
["directory_list"] = 4_000,
["multi_read"] = 20_000,
// 기타 도구: 기본 6000자
};
public static int GetBudget(string toolName)
=> DefaultBudgets.GetValueOrDefault(toolName, 6_000);
}
// ToolOutputSpillover.cs
public class ToolOutputSpillover
{
private readonly string _tempDir = Path.Combine(Path.GetTempPath(), "AxCopilot", "spill");
/// <summary>출력이 예산 초과 시 임시파일에 저장하고 경로+미리보기 반환.</summary>
public async Task<string> ProcessAsync(string toolName, string output, CancellationToken ct)
{
var budget = ToolOutputBudget.GetBudget(toolName);
if (output.Length <= budget) return output;
var path = Path.Combine(_tempDir, $"{Guid.NewGuid():N}.txt");
Directory.CreateDirectory(_tempDir);
await File.WriteAllTextAsync(path, output, ct);
var preview = output[..Math.Min(500, output.Length)];
return $"[출력이 {output.Length:N0}자로 예산({budget:N0}자)을 초과하여 임시파일에 저장됨]\n" +
$"파일 경로: {path}\n미리보기:\n{preview}\n...";
}
}
컨텍스트 조립 메모이제이션
// ContextAssemblyCache.cs
public class ContextAssemblyCache
{
private string? _cachedGitState;
private DateTime _gitStateCachedAt;
private MemoryContext? _cachedMemory;
private string? _cachedWorkFolder;
/// <summary>세션 내 git 상태는 5분 캐시.</summary>
public async Task<string> GetGitStateAsync(string workFolder, CancellationToken ct) { ... }
/// <summary>메모리 파일은 세션 시작 시 한 번만 로드 (변경 감지 시 갱신).</summary>
public async Task<MemoryContext> GetMemoryContextAsync(string workFolder, CancellationToken ct) { ... }
/// <summary>파일 변경 감지 시 캐시 무효화.</summary>
public void Invalidate() { _cachedMemory = null; _cachedGitState = null; }
}
Phase 19-G — /init 슬래시 명령 (v2.1)
목표: 프로젝트 분석 → AX.md 자동 생성 + 스킬/훅 초기 설정 제안.
핵심 클래스
// ProjectInitAnalyzer.cs
public class ProjectInitAnalyzer
{
/// <summary>
/// 프로젝트 구조를 분석하여 AX.md 초안을 생성합니다.
/// - 언어/프레임워크 탐지 (*.csproj, package.json, Cargo.toml 등)
/// - 테스트 프레임워크 탐지
/// - CI/CD 설정 탐지 (.github/workflows 등)
/// - 디렉터리 구조 요약
/// - 기존 README.md 요약 (LLM 호출)
/// </summary>
public async Task<ProjectAnalysis> AnalyzeAsync(string workFolder, CancellationToken ct) { ... }
/// <summary>분석 결과를 바탕으로 AX.md 초안 생성.</summary>
public async Task<string> GenerateAxMdAsync(ProjectAnalysis analysis,
LlmService llm, CancellationToken ct) { ... }
}
public record ProjectAnalysis(
string Language,
string Framework,
string? TestFramework,
string? CiSystem,
IReadOnlyList<string> KeyDirectories,
string? ReadmeSummary);
// /init 슬래시 명령
public class InitSlashCommand : ISlashCommand
{
public string Name => "init";
public string Description => "프로젝트를 분석하여 AX.md를 생성합니다";
public async Task ExecuteAsync(string args, IChatContext ctx)
{
// 1. 분석 실행 + 진행 상황 스트리밍
// 2. 생성된 AX.md 미리보기 표시
// 3. 사용자 확인 후 파일 저장
// 4. 필요 시 추천 스킬 제안
}
}
지원 언어/프레임워크 탐지
| 탐지 파일 | 언어 | 프레임워크 |
|---|---|---|
*.csproj, *.sln |
C# | .NET, ASP.NET, WPF |
package.json |
JavaScript/TypeScript | React, Vue, Next.js |
Cargo.toml |
Rust | — |
pyproject.toml, requirements.txt |
Python | Django, FastAPI, Flask |
pom.xml, build.gradle |
Java/Kotlin | Spring |
go.mod |
Go | — |
Phase 20 — SDK 제어 프로토콜 (v2.2)
목표: AX Agent를 외부 툴/스크립트에서 임베드 가능한 JSON 스트리밍 프로토콜로 노출.
| # | 기능 | 핵심 클래스 |
|---|---|---|
| 20-A | 양방향 JSON 스트리밍 서버 | AgentSdkServer, SdkControlRequest, SdkMessage |
| 20-B | 커스텀 에이전트 정의 (세션별) | CustomAgentDefinition, AgentTypeRegistry |
| 20-C | SDK 훅 콜백 (외부 권한 처리) | SdkHookCallbackHandler, SdkPermissionDelegate |
// SdkControlRequest 타입
public enum SdkControlRequestType
{
Initialize, // 세션 설정, 커스텀 에이전트 정의, 훅 등록
SetPermissionMode, // 권한 모드 변경
SetModel, // 모델 전환
Interrupt, // 현재 턴 취소
CanUseTool, // 권한 처리기 응답
GetContextUsage, // 컨텍스트 사용량 조회
RewindFiles, // 메시지 이후 파일 변경 되돌리기
HookCallback, // SDK측 훅 이벤트 전달
}
// SdkMessage (스트리밍 출력)
public enum SdkMessageType
{
AssistantToken, // 스트리밍 텍스트
ToolProgress, // 도구 실행 진행
ToolResult, // 도구 결과
SessionResult, // 최종 결과
HookEvent, // 훅 이벤트 (SDK 처리 요청)
Notification, // 알림
}
Phase 21 — AX Agent UI 전면 개편 (v2.3)
목표: CLAUDE.md Section 13에 명시된 Claude.ai + Codex 스타일 레이아웃 전면 구현.
| # | 기능 | 핵심 컴포넌트 |
|---|---|---|
| 21-1 | 3-Pane 레이아웃 골격 | ChatWindow.xaml 리팩터링 |
| 21-2 | 좌측 사이드바 | AgentSidebarView (탭 세그먼트 + 프리셋 + 이력) |
| 21-3 | 세션 헤더 바 | AgentSessionHeaderBar (모델/Plan/권한/도구 칩) |
| 21-4 | 우측 설정 패널 | AgentSettingsPanel (SettingsWindow AX Agent 탭 완전 대체) |
| 21-5 | 고도화 입력 영역 | AgentInputArea (@멘션, /스킬, 하단 칩 열) |
| 21-6 | 메시지 버블 개선 | Claude.ai 스타일 (AI=배경 없음, 도구블록=접히는 Border) |
| 21-7 | SettingsWindow 정리 | AX Agent 탭 제거, 전역 설정만 유지 |
Phase 22 — 슬래시 명령 체계 완성 (v2.1) ✅ 완료
목표: Claude Code의 16종 슬래시 명령을 AX Agent에 구현.
/init(19-G)을 제외한 15종 명령 추가. 기존 스킬 기반/호출과 공존하되, 내장 명령은 스킬보다 우선 처리.구현 완료 (2026-04-03): ISlashCommand 인터페이스, SlashCommandRegistry, InputRouter, 13종 명령 (/compact, /clear, /memory, /model, /plan, /commit, /review, /mcp, /permissions, /hooks, /config, /skills, /help) + SlashAutoCompleteProvider.
22-1: ISlashCommand 인터페이스 + 레지스트리
// 슬래시 명령 인터페이스 (Command 패턴)
public interface ISlashCommand
{
string Name { get; } // "compact", "memory" 등
string[] Aliases { get; } // "/settings" → "/config" 별칭
string Description { get; }
string? ArgumentHint { get; } // 자동완성 힌트
bool RequiresActiveSession { get; } // 세션 없이도 사용 가능 여부
Task ExecuteAsync(string arguments, IAgentChatContext context, CancellationToken ct);
}
// 채팅 컨텍스트 인터페이스 (DIP: 명령이 UI에 직접 의존하지 않음)
public interface IAgentChatContext
{
AgentLoopService AgentLoop { get; }
SettingsService Settings { get; }
SkillLoaderService SkillLoader { get; }
HookRunnerService HookRunner { get; }
McpManagerService McpManager { get; }
AgentSessionRepository SessionRepo { get; }
LlmService Llm { get; }
string ActiveTab { get; } // "Chat" | "Cowork" | "Code"
string WorkFolder { get; }
// UI 상호작용 (ViewModel 위임)
Task SendSystemMessageAsync(string message);
Task<string?> PromptUserAsync(string question); // 사용자 입력 대기
Task ShowToastAsync(string message);
void ClearMessages();
void UpdateSessionHeader(); // 모델/모드 변경 시 헤더 갱신
}
// 슬래시 명령 레지스트리 (Factory + Registry)
public class SlashCommandRegistry
{
private readonly Dictionary<string, ISlashCommand> _commands = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _aliases = new(StringComparer.OrdinalIgnoreCase);
public void Register(ISlashCommand command)
{
_commands[command.Name] = command;
foreach (var alias in command.Aliases)
_aliases[alias] = command.Name;
}
public ISlashCommand? Resolve(string input)
{
// "/compact focus on API changes" → name="compact", args="focus on API changes"
var name = input.Split(' ', 2)[0].TrimStart('/');
if (_commands.TryGetValue(name, out var cmd)) return cmd;
if (_aliases.TryGetValue(name, out var canonical))
return _commands.GetValueOrDefault(canonical);
return null; // null → 스킬 검색으로 폴백
}
public IReadOnlyList<ISlashCommand> GetAll() => _commands.Values.ToList();
// 기본 명령 등록
public static SlashCommandRegistry CreateDefault(IAgentChatContext ctx)
{
var reg = new SlashCommandRegistry();
reg.Register(new CompactCommand());
reg.Register(new ClearCommand());
reg.Register(new MemoryCommand());
reg.Register(new ConfigCommand());
reg.Register(new HooksCommand());
reg.Register(new McpCommand());
reg.Register(new PermissionsCommand());
reg.Register(new ModelCommand());
reg.Register(new PlanCommand());
reg.Register(new CommitCommand());
reg.Register(new ReviewCommand());
reg.Register(new SkillsCommand());
reg.Register(new HelpCommand());
reg.Register(new InitCommand()); // 19-G에서 구현
return reg;
}
}
22-2: /compact — 컨텍스트 압축
public class CompactCommand : ISlashCommand
{
public string Name => "compact";
public string[] Aliases => Array.Empty<string>();
public string Description => "대화 기록을 요약하여 컨텍스트 사용량을 줄입니다";
public string? ArgumentHint => "[요약 지시사항]";
public bool RequiresActiveSession => true;
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
// 1. PreCompact 훅 실행
var hookResult = await ctx.HookRunner.RunAsync(HookEvent.PreCompact, new HookContext
{
Event = HookEvent.PreCompact,
SessionId = ctx.AgentLoop.CurrentSessionId,
}, ct);
if (hookResult.AnyBlocked)
{
await ctx.SendSystemMessageAsync($"[컴팩션 차단됨] {hookResult.CombinedContext}");
return;
}
// 2. 현재 대화 메시지를 LLM에게 요약 요청
var messages = ctx.AgentLoop.GetCurrentMessages();
var summaryPrompt = string.IsNullOrEmpty(arguments)
? "위 대화를 핵심 결정사항, 작업 파일, 남은 작업 중심으로 간결하게 요약하세요."
: $"위 대화를 다음 지시에 따라 요약하세요: {arguments}";
var summary = await ctx.Llm.GenerateAsync(
BuildCompactPrompt(messages, summaryPrompt), ct);
// 3. TaskState 갱신 (Working Memory 보존)
if (ctx.AgentLoop.TaskState != null)
await ctx.AgentLoop.TaskState.UpdateContextSummaryAsync(summary);
// 4. 메시지 목록을 요약 메시지 1개로 교체
ctx.AgentLoop.ReplaceMessagesWithSummary(summary);
// 5. PostCompact 훅 실행
await ctx.HookRunner.RunAsync(HookEvent.PostCompact, new HookContext
{
Event = HookEvent.PostCompact,
}, ct);
await ctx.SendSystemMessageAsync($"[컴팩션 완료] 대화가 요약되었습니다. 컨텍스트 사용량이 줄었습니다.");
}
private string BuildCompactPrompt(IReadOnlyList<ChatMessage> messages, string instruction)
{
var sb = new StringBuilder();
sb.AppendLine("# 대화 기록");
foreach (var msg in messages)
sb.AppendLine($"[{msg.Role}] {msg.Content[..Math.Min(msg.Content.Length, 500)]}");
sb.AppendLine();
sb.AppendLine(instruction);
return sb.ToString();
}
}
22-3: /clear — 대화 초기화
public class ClearCommand : ISlashCommand
{
public string Name => "clear";
public string[] Aliases => new[] { "reset", "new" };
public string Description => "대화 기록을 지우고 새 세션을 시작합니다";
public string? ArgumentHint => null;
public bool RequiresActiveSession => false;
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
// 1. 현재 세션 저장
await ctx.SessionRepo.SaveAsync(ctx.AgentLoop.CurrentSession);
// 2. UI 메시지 목록 초기화
ctx.ClearMessages();
// 3. 새 세션 시작
await ctx.AgentLoop.StartNewSessionAsync(ctx.ActiveTab, ct);
// 4. SessionStart 훅 발화 (source: "clear")
await ctx.HookRunner.RunAsync(HookEvent.SessionStart, new HookContext
{
Event = HookEvent.SessionStart,
SessionId = ctx.AgentLoop.CurrentSessionId,
// source = "clear"
}, ct);
await ctx.SendSystemMessageAsync("새 대화가 시작되었습니다.");
}
}
22-4: /memory — 메모리 파일 편집
public class MemoryCommand : ISlashCommand
{
public string Name => "memory";
public string[] Aliases => Array.Empty<string>();
public string Description => "AX.md 메모리 파일을 편집합니다 (전역/프로젝트/로컬)";
public string? ArgumentHint => "[global|project|local]";
public bool RequiresActiveSession => false;
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
// 범위 결정
var scope = arguments.Trim().ToLowerInvariant() switch
{
"global" or "user" => MemoryScope.User,
"local" => MemoryScope.Local,
_ => MemoryScope.Project, // 기본값
};
// 파일 경로 결정
var path = scope switch
{
MemoryScope.User => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".axcopilot", "AX.md"),
MemoryScope.Local => Path.Combine(ctx.WorkFolder, "AX.local.md"),
_ => Path.Combine(ctx.WorkFolder, "AX.md"),
};
// 파일이 없으면 템플릿 생성
if (!File.Exists(path))
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path,
$"# AX Copilot 프로젝트 메모리 ({scope})\n\n여기에 지시사항을 작성하세요.\n", ct);
}
// 외부 에디터 열기 (기본 텍스트 에디터)
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
await ctx.SendSystemMessageAsync(
$"메모리 파일을 열었습니다: `{path}`\n편집 후 저장하면 다음 세션부터 반영됩니다.");
}
}
public enum MemoryScope { User, Project, Local }
22-5: /model — 세션 모델 전환
public class ModelCommand : ISlashCommand
{
public string Name => "model";
public string[] Aliases => Array.Empty<string>();
public string Description => "현재 세션의 AI 모델을 변경합니다";
public string? ArgumentHint => "[모델명]";
public bool RequiresActiveSession => true;
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(arguments))
{
// 현재 모델 + 사용 가능한 모델 목록 표시
var current = ctx.AgentLoop.CurrentModel;
var available = ctx.Settings.Settings.Llm.RegisteredModels
.Select(m => m.DisplayName).ToList();
await ctx.SendSystemMessageAsync(
$"현재 모델: **{current}**\n사용 가능: {string.Join(", ", available)}");
return;
}
// 모델 변경
var success = ctx.AgentLoop.TrySetModel(arguments.Trim());
if (success)
{
ctx.UpdateSessionHeader();
await ctx.SendSystemMessageAsync($"모델이 **{arguments.Trim()}**(으)로 변경되었습니다.");
}
else
{
await ctx.SendSystemMessageAsync($"모델 '{arguments.Trim()}'을(를) 찾을 수 없습니다.");
}
}
}
22-6: /plan — 플랜 모드 토글
public class PlanCommand : ISlashCommand
{
public string Name => "plan";
public string[] Aliases => Array.Empty<string>();
public string Description => "플랜 모드를 토글하거나 계획을 생성합니다";
public string? ArgumentHint => "[open|<계획 설명>]";
public bool RequiresActiveSession => true;
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
var arg = arguments.Trim().ToLowerInvariant();
if (arg == "open")
{
// 현재 계획 표시
var plan = ctx.AgentLoop.CurrentPlan;
if (plan == null)
await ctx.SendSystemMessageAsync("현재 활성 계획이 없습니다.");
else
await ctx.SendSystemMessageAsync($"## 현재 계획\n{plan}");
return;
}
if (string.IsNullOrEmpty(arg))
{
// 플랜 모드 토글
var newMode = ctx.AgentLoop.TogglePlanMode();
ctx.UpdateSessionHeader();
await ctx.SendSystemMessageAsync(
$"플랜 모드: **{newMode}** ({newMode switch {
PlanMode.Off => "변경 사항을 바로 실행합니다",
PlanMode.Always => "실행 전 항상 계획을 작성하고 승인을 기다립니다",
PlanMode.Auto => "복잡한 작업에만 자동으로 계획을 작성합니다",
_ => "" }})");
return;
}
// 주어진 설명으로 계획 생성
await ctx.AgentLoop.CreatePlanAsync(arg, ct);
}
}
22-7: /commit — AI 커밋 메시지
public class CommitCommand : ISlashCommand
{
public string Name => "commit";
public string[] Aliases => Array.Empty<string>();
public string Description => "AI가 생성한 메시지로 git 커밋을 만듭니다";
public string? ArgumentHint => null;
public bool RequiresActiveSession => false;
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
// 1. git status + git diff 확인
var statusResult = await ProcessHelper.RunAsync("git", "status --porcelain", ctx.WorkFolder, ct);
if (string.IsNullOrWhiteSpace(statusResult.StdOut))
{
await ctx.SendSystemMessageAsync("커밋할 변경 사항이 없습니다.");
return;
}
var diffResult = await ProcessHelper.RunAsync("git", "diff --cached", ctx.WorkFolder, ct);
var diffAll = string.IsNullOrWhiteSpace(diffResult.StdOut)
? (await ProcessHelper.RunAsync("git", "diff", ctx.WorkFolder, ct)).StdOut
: diffResult.StdOut;
// 2. LLM에게 커밋 메시지 생성 요청
var prompt = $"""
아래 git diff를 분석하고 간결한 커밋 메시지를 작성하세요.
- 첫 줄: 50자 이내 제목 (무엇을 왜)
- 빈 줄 후 본문: 변경 이유와 영향 (선택)
- 비밀 파일(.env 등) 포함 여부 경고
```diff
{diffAll[..Math.Min(diffAll.Length, 8000)]}
```
git status:
{statusResult.StdOut}
""";
var commitMsg = await ctx.Llm.GenerateAsync(prompt, ct);
// 3. 사용자 확인
await ctx.SendSystemMessageAsync($"## 제안된 커밋 메시지\n```\n{commitMsg}\n```\n\n이 메시지로 커밋할까요? (yes/no)");
var answer = await ctx.PromptUserAsync("커밋 확인");
if (answer?.Trim().ToLowerInvariant() is not ("yes" or "y" or "ㅇ"))
{
await ctx.SendSystemMessageAsync("커밋이 취소되었습니다.");
return;
}
// 4. git add + commit 실행
await ProcessHelper.RunAsync("git", "add -A", ctx.WorkFolder, ct);
var commitResult = await ProcessHelper.RunAsync("git",
$"commit -m \"{commitMsg.Replace("\"", "\\\"")}\"", ctx.WorkFolder, ct);
await ctx.SendSystemMessageAsync(
commitResult.ExitCode == 0
? $"커밋 완료: {commitResult.StdOut}"
: $"커밋 실패: {commitResult.StdErr}");
}
}
22-8: /mcp — MCP 서버 관리
public class McpCommand : ISlashCommand
{
public string Name => "mcp";
public string[] Aliases => Array.Empty<string>();
public string Description => "MCP 서버 상태 확인 및 활성화/비활성화";
public string? ArgumentHint => "[enable|disable [서버명]]";
public bool RequiresActiveSession => false;
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
var parts = arguments.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var action = parts.Length > 0 ? parts[0].ToLowerInvariant() : "";
var serverName = parts.Length > 1 ? parts[1] : null;
if (string.IsNullOrEmpty(action))
{
// 상태 표시
var sb = new StringBuilder("## MCP 서버 상태\n");
foreach (var client in ctx.McpManager.GetAllClients())
{
var status = client.IsConnected ? "🟢 연결됨" : "🔴 끊김";
sb.AppendLine($"- **{client.ServerName}** {status} ({client.Tools.Count}개 도구)");
}
await ctx.SendSystemMessageAsync(sb.ToString());
return;
}
switch (action)
{
case "enable":
if (serverName != null)
await ctx.McpManager.EnableServerAsync(serverName, ct);
else
await ctx.McpManager.EnableAllAsync(ct);
await ctx.SendSystemMessageAsync($"MCP 서버 활성화: {serverName ?? "전체"}");
break;
case "disable":
if (serverName != null)
ctx.McpManager.DisableServer(serverName);
else
ctx.McpManager.DisableAll();
await ctx.SendSystemMessageAsync($"MCP 서버 비활성화: {serverName ?? "전체"}");
break;
case "reconnect":
if (serverName != null)
await ctx.McpManager.ReconnectAsync(serverName, ct);
await ctx.SendSystemMessageAsync($"MCP 서버 재연결: {serverName}");
break;
default:
await ctx.SendSystemMessageAsync("사용법: /mcp [enable|disable|reconnect] [서버명]");
break;
}
}
}
22-9: /permissions, /hooks, /config, /skills, /review, /help
// /permissions — 권한 규칙 관리
public class PermissionsCommand : ISlashCommand
{
public string Name => "permissions";
public string[] Aliases => new[] { "allowed-tools" };
public string Description => "도구 허용/차단 규칙을 관리합니다";
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
// 현재 권한 모드 + allow/deny 규칙 목록 표시
// "allow Bash(git *)" / "deny Bash(rm -rf *)" 형식 인수 처리
var settings = ctx.Settings.Settings.Llm;
var sb = new StringBuilder("## 권한 설정\n");
sb.AppendLine($"현재 모드: **{settings.PermissionMode}**\n");
sb.AppendLine("### 허용 규칙");
foreach (var rule in settings.AllowRules)
sb.AppendLine($"- ✅ `{rule}`");
sb.AppendLine("\n### 차단 규칙");
foreach (var rule in settings.DenyRules)
sb.AppendLine($"- ❌ `{rule}`");
await ctx.SendSystemMessageAsync(sb.ToString());
}
}
// /hooks — 훅 설정 표시
public class HooksCommand : ISlashCommand
{
public string Name => "hooks";
public string[] Aliases => Array.Empty<string>();
public string Description => "활성화된 훅 설정을 표시합니다";
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
var hooks = ctx.HookRunner.GetAllDefinitions();
var sb = new StringBuilder("## 활성 훅\n");
foreach (var (eventName, defs) in hooks)
{
sb.AppendLine($"### {eventName}");
foreach (var def in defs)
{
var matcher = string.IsNullOrEmpty(def.Matcher) ? "(전체)" : def.Matcher;
sb.AppendLine($"- [{def.Type}] {matcher}: `{def.Command ?? def.Prompt ?? def.Url}`");
}
}
await ctx.SendSystemMessageAsync(sb.ToString());
}
}
// /config — 설정 열기
public class ConfigCommand : ISlashCommand
{
public string Name => "config";
public string[] Aliases => new[] { "settings" };
public string Description => "설정 화면을 엽니다";
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
// 인라인 설정 패널 토글 (Phase 21 UI) 또는 SettingsWindow 열기
ctx.ToggleSettingsPanel();
}
}
// /skills — 스킬 목록
public class SkillsCommand : ISlashCommand
{
public string Name => "skills";
public string[] Aliases => Array.Empty<string>();
public string Description => "사용 가능한 스킬 목록을 표시합니다";
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
var skills = ctx.SkillLoader.GetUserInvocableSkills();
var sb = new StringBuilder("## 사용 가능한 스킬\n");
foreach (var skill in skills)
{
var hint = string.IsNullOrEmpty(skill.Frontmatter.ArgumentHint)
? "" : $" `{skill.Frontmatter.ArgumentHint}`";
sb.AppendLine($"- **/{skill.Name}**{hint} — {skill.Frontmatter.Description}");
}
await ctx.SendSystemMessageAsync(sb.ToString());
}
}
// /review — PR 리뷰
public class ReviewCommand : ISlashCommand
{
public string Name => "review";
public string[] Aliases => Array.Empty<string>();
public string Description => "AI 코드 리뷰를 실행합니다";
public string? ArgumentHint => "[PR 번호 또는 diff]";
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
// git diff 가져오기
string diff;
if (!string.IsNullOrWhiteSpace(arguments) && int.TryParse(arguments.Trim(), out var prNum))
{
// GitHub PR diff 가져오기 (gh CLI 사용)
var result = await ProcessHelper.RunAsync("gh", $"pr diff {prNum}", ctx.WorkFolder, ct);
diff = result.StdOut;
}
else
{
// 현재 브랜치 diff
var result = await ProcessHelper.RunAsync("git",
"diff main...HEAD", ctx.WorkFolder, ct);
diff = result.StdOut;
}
if (string.IsNullOrWhiteSpace(diff))
{
await ctx.SendSystemMessageAsync("리뷰할 변경 사항이 없습니다.");
return;
}
// LLM 코드 리뷰 요청
var prompt = $"""
아래 코드 변경 사항을 리뷰하세요:
1. 변경 사항 개요
2. 코드 품질 및 스타일
3. 잠재적 버그 또는 보안 문제
4. 개선 제안
```diff
{diff[..Math.Min(diff.Length, 12000)]}
```
""";
var review = await ctx.Llm.GenerateAsync(prompt, ct);
await ctx.SendSystemMessageAsync($"## 코드 리뷰\n{review}");
}
}
// /help — 도움말
public class HelpCommand : ISlashCommand
{
public string Name => "help";
public string[] Aliases => Array.Empty<string>();
public string Description => "사용 가능한 명령 목록을 표시합니다";
public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
{
var registry = ctx.SlashCommandRegistry;
var sb = new StringBuilder("## AX Agent 명령\n\n");
foreach (var cmd in registry.GetAll().OrderBy(c => c.Name))
{
var hint = cmd.ArgumentHint != null ? $" {cmd.ArgumentHint}" : "";
var aliases = cmd.Aliases.Length > 0
? $" (별칭: {string.Join(", ", cmd.Aliases.Select(a => $"/{a}"))})" : "";
sb.AppendLine($"- **/{cmd.Name}**{hint} — {cmd.Description}{aliases}");
}
sb.AppendLine("\n스킬 명령은 `/skills`로 확인하세요.");
await ctx.SendSystemMessageAsync(sb.ToString());
}
}
22-10: 입력 라우팅 통합
// AgentLoopService 또는 AgentWindowViewModel에서 입력 처리
public class InputRouter
{
private readonly SlashCommandRegistry _slashCommands;
private readonly SkillLoaderService _skillLoader;
/// <summary>사용자 입력을 슬래시 명령 → 스킬 → 일반 메시지 순으로 라우팅.</summary>
public async Task<InputRouteResult> RouteAsync(string input, IAgentChatContext ctx, CancellationToken ct)
{
if (!input.StartsWith("/"))
return InputRouteResult.Message(input); // 일반 메시지
var spaceIdx = input.IndexOf(' ');
var commandPart = spaceIdx > 0 ? input[..spaceIdx] : input;
var argsPart = spaceIdx > 0 ? input[(spaceIdx + 1)..] : "";
// 1. 내장 슬래시 명령 확인
var cmd = _slashCommands.Resolve(commandPart);
if (cmd != null)
{
await cmd.ExecuteAsync(argsPart, ctx, ct);
return InputRouteResult.Handled;
}
// 2. 스킬 검색
var skillName = commandPart.TrimStart('/');
var skill = _skillLoader.FindByName(skillName);
if (skill != null)
return InputRouteResult.Skill(skill, argsPart);
// 3. 미인식 → 일반 메시지로 전달
return InputRouteResult.Message(input);
}
}
public record InputRouteResult(InputRouteType Type, string? Message = null,
LoadedSkill? Skill = null, string? SkillArgs = null)
{
public static InputRouteResult Handled => new(InputRouteType.Handled);
public static InputRouteResult Message(string msg) => new(InputRouteType.Message, msg);
public static InputRouteResult Skill(LoadedSkill skill, string args)
=> new(InputRouteType.Skill, Skill: skill, SkillArgs: args);
}
public enum InputRouteType { Handled, Message, Skill }
Phase 19-D 확장 — 스킬 시스템 CC 동등성 보완 (v2.1)
기존 17-D에 없는 3가지 핵심 기능 추가: 인라인 셸 커맨드, 네임스페이싱, 명명된 인수
19-D-EXT-1: 인라인 셸 커맨드 (!backtick`)
// SkillInlineCommandProcessor.cs
public class SkillInlineCommandProcessor
{
// !`command` 패턴을 찾아 실행하고 출력으로 교체
private static readonly Regex InlineCmdPattern = new(@"!\`([^`]+)\`", RegexOptions.Compiled);
/// <summary>
/// 스킬 본문에서 !`command` 패턴을 찾아 셸 실행 결과로 교체합니다.
/// 호출 시점: 스킬이 로드된 후, LLM에 프롬프트로 전달하기 직전.
/// </summary>
public async Task<string> ProcessAsync(string skillBody, string workFolder, CancellationToken ct)
{
var matches = InlineCmdPattern.Matches(skillBody);
if (matches.Count == 0) return skillBody;
var result = skillBody;
foreach (Match match in matches.Reverse()) // 뒤에서부터 교체 (인덱스 유지)
{
var command = match.Groups[1].Value;
var output = await ExecuteCommandAsync(command, workFolder, ct);
result = result.Remove(match.Index, match.Length)
.Insert(match.Index, output);
}
return result;
}
private async Task<string> ExecuteCommandAsync(string command, string workFolder, CancellationToken ct)
{
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(10)); // 인라인 명령 타임아웃 10초
var psi = new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "bash",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? $"/c {command}" : $"-c \"{command.Replace("\"", "\\\"")}\"",
WorkingDirectory = workFolder,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
using var proc = Process.Start(psi);
if (proc == null) return "[실행 실패]";
var stdout = await proc.StandardOutput.ReadToEndAsync(cts.Token);
await proc.WaitForExitAsync(cts.Token);
return stdout.Trim();
}
catch (OperationCanceledException)
{
return "[타임아웃]";
}
catch (Exception ex)
{
return $"[오류: {ex.Message}]";
}
}
}
19-D-EXT-2: 스킬 네임스페이싱 (서브디렉토리 콜론 구분)
// SkillLoaderService 확장
public class SkillLoaderService
{
// 기존 메서드 + 네임스페이싱 로직
/// <summary>
/// 스킬 디렉토리 구조를 콜론 구분 네임스페이스로 변환합니다.
/// .claude/skills/database/migrate/SKILL.md → "database:migrate"
/// .claude/skills/deploy/SKILL.md → "deploy"
/// </summary>
private string ResolveSkillName(string skillDir, string baseSkillsDir)
{
var relative = Path.GetRelativePath(baseSkillsDir, skillDir);
// Windows 경로 구분자 → 콜론
return relative.Replace(Path.DirectorySeparatorChar, ':')
.Replace(Path.AltDirectorySeparatorChar, ':');
}
/// <summary>콜론 구분 이름으로 스킬 검색 (부분 일치 지원).</summary>
public LoadedSkill? FindByName(string name)
{
// 정확한 일치
var exact = _skills.FirstOrDefault(s =>
s.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (exact != null) return exact;
// 마지막 세그먼트 일치 (database:migrate → "migrate"로도 검색 가능)
var lastSegment = name.Contains(':') ? name : name;
return _skills.FirstOrDefault(s =>
s.Name.EndsWith($":{lastSegment}", StringComparison.OrdinalIgnoreCase) ||
s.Name.Equals(lastSegment, StringComparison.OrdinalIgnoreCase));
}
// 디렉토리 스캔 시 재귀적으로 SKILL.md 검색
private void ScanSkillDirectory(string dir, string baseDir, List<LoadedSkill> results)
{
var skillFile = Path.Combine(dir, "SKILL.md");
if (File.Exists(skillFile))
{
var name = ResolveSkillName(dir, baseDir);
var content = File.ReadAllText(skillFile);
var (frontmatter, body) = ParseFrontmatter(content);
results.Add(new LoadedSkill(name, frontmatter, body, skillFile));
}
foreach (var subDir in Directory.GetDirectories(dir))
ScanSkillDirectory(subDir, baseDir, results);
}
}
19-D-EXT-3: 명명된 인수 ($name 구문)
// SkillArgumentSubstitution.cs
public static class SkillArgumentSubstitution
{
/// <summary>
/// 스킬 본문에서 $ARGUMENTS와 명명된 인수를 치환합니다.
/// - $ARGUMENTS → 전체 인수 문자열
/// - $name → 프론트매터 arguments: [name, directory] 기반 위치 매핑
/// - {0}, {1} → 인덱스 기반 인수
/// </summary>
public static string Substitute(string body, SkillFrontmatter frontmatter, string userInput)
{
var result = body;
// 1. $ARGUMENTS → 전체 인수
result = result.Replace("$ARGUMENTS", userInput);
// 2. 명명된 인수 (프론트매터에 arguments가 정의된 경우)
if (frontmatter.Arguments.Count > 0)
{
var parts = SplitArguments(userInput);
for (var i = 0; i < frontmatter.Arguments.Count; i++)
{
var argName = frontmatter.Arguments[i];
var argValue = i < parts.Count ? parts[i] : "";
result = result.Replace($"${argName}", argValue);
}
}
// 3. 인덱스 기반 인수 ({0}, {1}, ...)
var indexParts = SplitArguments(userInput);
for (var i = 0; i < indexParts.Count; i++)
result = result.Replace($"{{{i}}}", indexParts[i]);
return result;
}
private static List<string> SplitArguments(string input)
{
// 따옴표로 감싼 인수 지원: "hello world" foo bar
var parts = new List<string>();
var current = new StringBuilder();
var inQuote = false;
foreach (var ch in input)
{
if (ch == '"') { inQuote = !inQuote; continue; }
if (ch == ' ' && !inQuote && current.Length > 0)
{
parts.Add(current.ToString());
current.Clear();
continue;
}
current.Append(ch);
}
if (current.Length > 0) parts.Add(current.ToString());
return parts;
}
}
19-D-EXT-4: SkillExecutionPipeline 통합
// 스킬 실행 파이프라인: 인수 치환 → 인라인 셸 → LLM 전달
public class SkillExecutionPipeline
{
private readonly SkillInlineCommandProcessor _inlineCmd;
private readonly PathBasedSkillActivator _pathActivator;
private readonly ForkContextSkillRunner _forkRunner;
private readonly HookRunnerService _hookRunner;
public async Task<SkillResult> ExecuteAsync(
LoadedSkill skill, string userInput, string workFolder,
AgentLoopService agentLoop, CancellationToken ct)
{
// 1. PreSkillExecute 훅
await _hookRunner.RunAsync(HookEvent.PreSkillExecute, new HookContext
{
Event = HookEvent.PreSkillExecute,
ToolName = skill.Name,
UserMessage = userInput,
}, ct);
// 2. 인수 치환 ($ARGUMENTS, $name, {0})
var body = SkillArgumentSubstitution.Substitute(
skill.Body, skill.Frontmatter, userInput);
// 3. 인라인 셸 명령 실행 (!`command`)
body = await _inlineCmd.ProcessAsync(body, workFolder, ct);
// 4. 모델 오버라이드 확인
var model = skill.Frontmatter.Model;
// 5. 컨텍스트 분기 여부 확인
if (skill.Frontmatter.Context == SkillContext.Fork)
{
var result = await _forkRunner.ExecuteInForkAsync(skill, body, ct);
// PostSkillExecute 훅
await _hookRunner.RunAsync(HookEvent.PostSkillExecute, new HookContext
{
Event = HookEvent.PostSkillExecute,
ToolName = skill.Name,
ToolOutput = result.Output,
}, ct);
return result;
}
// 6. 메인 컨텍스트에서 실행 (body를 시스템 프롬프트에 주입)
return new SkillResult
{
IsSuccess = true,
Output = body, // AgentLoopService가 이를 프롬프트로 사용
};
}
}
Phase 19-B 확장 — 복합 Bash 명령 파싱 (v2.1)
기존 19-B에 누락된 복합 명령 파싱 상세 구현
19-B-EXT-1: CompoundCommandParser
// 복합 Bash 명령을 서브커맨드로 분리하여 각각 권한 검사
public class CompoundCommandParser
{
// &&, ||, ;, | 로 분리 (따옴표, 서브셸 내부는 분리하지 않음)
public static IReadOnlyList<string> Parse(string command)
{
var result = new List<string>();
var current = new StringBuilder();
var inSingleQuote = false;
var inDoubleQuote = false;
var parenDepth = 0;
for (var i = 0; i < command.Length; i++)
{
var ch = command[i];
var next = i + 1 < command.Length ? command[i + 1] : '\0';
// 따옴표 토글
if (ch == '\'' && !inDoubleQuote) { inSingleQuote = !inSingleQuote; current.Append(ch); continue; }
if (ch == '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; current.Append(ch); continue; }
// 서브셸
if (!inSingleQuote && !inDoubleQuote)
{
if (ch == '(') parenDepth++;
if (ch == ')') parenDepth--;
}
// 분리자 검출 (따옴표/서브셸 밖에서만)
if (!inSingleQuote && !inDoubleQuote && parenDepth == 0)
{
if ((ch == '&' && next == '&') || (ch == '|' && next == '|'))
{
if (current.Length > 0) result.Add(current.ToString().Trim());
current.Clear();
i++; // 두 글자 연산자 건너뜀
continue;
}
if (ch == ';' || ch == '|')
{
if (current.Length > 0) result.Add(current.ToString().Trim());
current.Clear();
continue;
}
}
current.Append(ch);
}
if (current.Length > 0) result.Add(current.ToString().Trim());
return result;
}
}
// PermissionEvaluator 확장
public class PermissionEvaluator
{
private readonly IReadOnlyList<PermissionRule> _allowRules;
private readonly IReadOnlyList<PermissionRule> _denyRules;
/// <summary>
/// 복합 명령의 각 서브커맨드를 독립적으로 검사합니다.
/// 하나의 서브커맨드라도 deny되면 전체 명령이 차단됩니다.
/// </summary>
public PermissionDecision EvaluateCompound(string toolName, string command)
{
if (toolName != "bash" && toolName != "process")
return EvaluateSingle(toolName, command);
var subCommands = CompoundCommandParser.Parse(command);
foreach (var sub in subCommands)
{
var decision = EvaluateSingle(toolName, sub.Trim());
if (decision == PermissionDecision.Deny)
return PermissionDecision.Deny;
}
// 모든 서브커맨드가 allow이면 allow, 아니면 ask
return subCommands.All(sub => EvaluateSingle(toolName, sub) == PermissionDecision.Allow)
? PermissionDecision.Allow
: PermissionDecision.Ask;
}
// 항상 차단되는 위험 패턴
private static readonly string[] AlwaysEscalate = new[]
{
"sed -i", // 파일 인플레이스 편집
"rm -rf /", // 루트 삭제
"chmod 777", // 위험한 권한
"> ~/.bashrc", // 셸 설정 변경
"> ~/.zshrc",
".claude/", // 설정 디렉토리 조작
".git/", // git 내부 조작
};
}
Phase 17-C 확장 — 훅 환경변수 시스템 (v2.1)
CC의 훅 환경변수 체계를 구현: $CLAUDE_FILE_PATH, $CLAUDE_TOOL_NAME 등
17-C-EXT-1: HookEnvironmentBuilder
// 훅 실행 시 셸 환경변수를 자동 주입
public static class HookEnvironmentBuilder
{
/// <summary>HookContext에서 셸 환경변수 딕셔너리를 생성합니다.</summary>
public static Dictionary<string, string> Build(HookContext context)
{
var env = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// 공통 변수
env["AX_SESSION_ID"] = context.SessionId;
env["AX_HOOK_EVENT"] = context.Event.ToString();
env["AX_CWD"] = context.WorkFolder ?? "";
env["AX_PERMISSION_MODE"] = context.PermissionMode ?? "default";
// 도구 관련 변수 (PreToolUse, PostToolUse)
if (!string.IsNullOrEmpty(context.ToolName))
{
env["AX_TOOL_NAME"] = context.ToolName;
env["CLAUDE_TOOL_NAME"] = context.ToolName; // CC 호환
}
if (!string.IsNullOrEmpty(context.ToolInput))
{
env["AX_TOOL_INPUT"] = context.ToolInput;
env["CLAUDE_TOOL_INPUT"] = context.ToolInput; // CC 호환
}
// 파일 경로 변수 (Write, Edit 도구 시)
if (!string.IsNullOrEmpty(context.FilePath))
{
env["AX_FILE_PATH"] = context.FilePath;
env["CLAUDE_FILE_PATH"] = context.FilePath; // CC 호환
}
// 파일 변경 변수 (FileChanged)
if (!string.IsNullOrEmpty(context.ChangedFilePath))
env["AX_CHANGED_FILE"] = context.ChangedFilePath;
// CwdChanged 전용: CLAUDE_ENV_FILE
if (context.Event == HookEvent.CwdChanged)
{
var envFile = Path.Combine(Path.GetTempPath(), "AxCopilot",
$"env_{context.SessionId}.txt");
Directory.CreateDirectory(Path.GetDirectoryName(envFile)!);
env["CLAUDE_ENV_FILE"] = envFile;
env["AX_ENV_FILE"] = envFile;
}
// 사용자 프롬프트 (UserPromptSubmit)
if (!string.IsNullOrEmpty(context.UserMessage))
env["AX_USER_PROMPT"] = context.UserMessage;
return env;
}
}
// CommandHookExecutor에서 환경변수 주입
public class CommandHookExecutor : IHookExecutor
{
public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
{
var psi = new ProcessStartInfo { ... };
// 환경변수 주입
foreach (var (key, value) in HookEnvironmentBuilder.Build(context))
psi.Environment[key] = value;
// stdin에 JSON 입력 전달 (CC 호환)
var inputJson = JsonSerializer.Serialize(context, _jsonOpts);
// ... 프로세스 실행, stdin에 inputJson 쓰기 ...
}
}
Phase 23 — 최종 CC 동등성 검증 + 누락 기능 (v2.1) ✅ 완료
목표: Phase 19~22 이후 남은 소규모 갭을 해소하고, 전체 기능 매트릭스 검증
구현 완료 (2026-04-03): AutoCompactMonitor, IConditionalTool + ToolEnvironmentContext, SlashAutoCompleteProvider. 또한 Phase 19-D-EXT(SkillArgumentSubstitution, SkillInlineCommandProcessor → SkillService 통합), Phase 19-B-EXT(CompoundCommandParser → PermissionDecisionService 통합), Phase 17-C-EXT(HookEnvironmentBuilder → ExtendedHookRunner 통합) 모두 완료.
23-1: argument-hint 자동완성 UI
// AgentInputArea에서 / 입력 시 자동완성 팝업에 hint 표시
public class SlashAutoCompleteProvider
{
private readonly SlashCommandRegistry _commands;
private readonly SkillLoaderService _skills;
public IReadOnlyList<AutoCompleteItem> GetSuggestions(string input)
{
if (!input.StartsWith("/")) return Array.Empty<AutoCompleteItem>();
var prefix = input.TrimStart('/');
var results = new List<AutoCompleteItem>();
// 내장 명령
foreach (var cmd in _commands.GetAll())
{
if (cmd.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
results.Add(new AutoCompleteItem($"/{cmd.Name}", cmd.Description, cmd.ArgumentHint));
}
// 스킬
foreach (var skill in _skills.GetUserInvocableSkills())
{
if (skill.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
results.Add(new AutoCompleteItem($"/{skill.Name}",
skill.Frontmatter.Description, skill.Frontmatter.ArgumentHint));
}
return results.OrderBy(r => r.Name).ToList();
}
}
public record AutoCompleteItem(string Name, string Description, string? ArgumentHint);
23-2: 세션 /compact 자동 트리거
// 컨텍스트 크기 모니터링 → 자동 컴팩션
public class AutoCompactMonitor
{
private readonly TokenEstimatorService _tokenEstimator;
private readonly int _thresholdPercent; // 기본 80%
/// <summary>매 에이전트 반복 후 호출 → 컨텍스트가 임계치 초과 시 자동 컴팩션.</summary>
public async Task<bool> CheckAndCompactAsync(
AgentLoopService agentLoop, LlmService llm, CancellationToken ct)
{
var usage = _tokenEstimator.EstimateTokenCount(agentLoop.GetCurrentMessages());
var maxTokens = agentLoop.CurrentModelMaxTokens;
var usagePercent = (int)((double)usage / maxTokens * 100);
if (usagePercent < _thresholdPercent) return false;
LogService.Info($"컨텍스트 {usagePercent}% 사용 → 자동 컴팩션 시작");
// CompactCommand와 동일한 로직 (DRY: 공유 서비스로 분리)
await CompactionService.CompactAsync(agentLoop, llm, instruction: null, ct);
return true;
}
}
// 설정 추가
public class LlmSettings
{
[JsonPropertyName("auto_compact_enabled")]
public bool AutoCompactEnabled { get; set; } = true;
[JsonPropertyName("auto_compact_threshold_percent")]
public int AutoCompactThresholdPercent { get; set; } = 80;
}
23-3: 도구별 isEnabled() 자가 비활성화
// IAgentTool 인터페이스 확장 (ISP 준수: 옵션 인터페이스)
public interface IConditionalTool
{
/// <summary>현재 환경에서 이 도구가 사용 가능한지 확인합니다.</summary>
bool IsEnabled(ToolEnvironmentContext env);
}
public record ToolEnvironmentContext(
AppSettings Settings,
string ActiveTab, // "Chat" | "Cowork" | "Code"
string? WorkFolder,
bool HasGitRepo,
bool HasNodeRuntime,
bool HasPythonRuntime);
// ToolRegistry에서 도구 목록 생성 시 필터링
public class ToolRegistry
{
public IReadOnlyList<IAgentTool> GetActiveTools(ToolEnvironmentContext env)
{
return _tools.Where(t =>
{
if (t is IConditionalTool conditional)
return conditional.IsEnabled(env);
return true; // 조건 없는 도구는 항상 활성
}).ToList();
}
}
23-4: CC 기능 매트릭스 최종 검증표
| CC 기능 | AX Phase | 상태 |
|---|---|---|
| 4-layer memory (managed/user/project/local) | 19-A | ✅ Phase 22 구현 |
| @include 5단계 | 기존 | ✅ 구현됨 |
| rules/*.md + paths frontmatter | 기존 | ✅ 구현됨 |
| 18종 훅 이벤트 | 25 | ✅ Phase 25 구현 (PostToolUseFailure, Stop 포함) |
| 4종 훅 타입 (command/http/prompt/agent) | 25 | ✅ Phase 25 구현 |
| 훅 환경변수 ($CLAUDE_FILE_PATH 등) | 25 | ✅ Phase 25 HookEnvironmentBuilder |
| 훅 if 조건부 실행 | 25 | ✅ Phase 25 EvaluateCondition (==, !=, contains, &&) |
| 훅 once/async/asyncRewake | 25 | ✅ Phase 25 AsyncRewakeTriggered 이벤트 |
| 스킬 $ARGUMENTS 치환 | 24 | ✅ Phase 24 SkillArgumentSubstitution |
| 스킬 인라인 셸 실행 | 24 | ✅ Phase 24 SkillInlineCommandProcessor |
| 스킬 네임스페이싱 (colon) | 24 | ✅ Phase 24 LoadSkillsRecursive |
| 스킬 명명된 인수 ($name) | 24 | ✅ Phase 24 SkillDefinition.Arguments |
| 스킬 context:fork | 24 | ✅ Phase 24 IsForkContext frontmatter |
| 스킬 paths 자동 활성화 | 24 | ✅ Phase 24 PathBasedSkillActivator |
| 스킬 model 오버라이드 | 24 | ✅ Phase 24 ModelOverride frontmatter |
| 스킬 user-invocable | 24 | ✅ Phase 24 UserInvocable 필터링 |
| 스킬 범위 훅 | 24 | ✅ Phase 24 ScopedHooks frontmatter |
| 4종 권한 모드 (default/acceptEdits/plan/bypass) | 22 | ✅ Phase 22 구현 |
| 복합 Bash 명령 파싱 (&&/||/;/|) | 22 | ✅ Phase 22 CompoundCommandParser |
| glob 패턴 권한 매칭 (Bash(git *)) | 기존 | ✅ 구현됨 |
| MCP 도구 차단 (mcp__server 규칙) | 기존 | ✅ 구현됨 |
| 도구 가시성 필터링 | 기존 | ✅ 구현됨 |
| 슬래시 명령 14종 | 22 + 26 | ✅ Phase 22/26 SlashCommandRegistry |
| 출력 예산 + 스필오버 | 27 | ✅ Phase 27 ToolResultSizer |
| 세션 fork/resume/tag | 28 | ✅ Phase 28 SessionManager |
| 자동 컴팩션 | 29-A | ✅ Phase 29 AutoCompactMonitor 통합 |
| 도구 isEnabled() 자가 비활성화 | 29-B | ✅ Phase 29 IConditionalTool + ToolEnvironmentContext |
| /init 프로젝트 초기화 | 26 | ✅ Phase 26 InitCommand |
| @include AxMd 지시어 | 27 | ✅ Phase 27 AxMdIncludeResolver |
| 멀티에이전트 + worktree | 기존 | ✅ SubAgentTool + DelegateAgentTool + WorktreeManager |
| 에이전트 타입별 메모리 | 기존 | ✅ AgentTypeMemoryRepository + MemoryTool + /memory 명령 |
| 4-layer 계층 메모리 통합 조회 | 30-C | ✅ Phase 30 HierarchicalMemoryService |
| SDK 제어 프로토콜 (JSON 스트리밍) | 31 | ✅ Phase 31 AgentSdkServer + SdkProtocol |
| 커스텀 에이전트 정의 (SDK) | 31-C | ✅ Phase 31 CustomAgentDefinition + AgentTypeRegistry |
| SDK 훅 콜백 (외부 권한 핸들링) | 31-D | ✅ Phase 31 SdkHookCallbackHandler |
| 3-Pane Claude.ai UI (사이드바+헤더+설정 패널+입력) | 32 | ✅ Phase 32 AgentSettingsPanel/SidebarView/HeaderBar/InputArea |
구현 의존성 그래프 (Phase 19+ 갱신)
[19-A] HierarchicalMemory ─────────────────────────────────────────┐
↓ │
[19-B] PermissionPatternMatch ← [19-B-EXT] CompoundParsing │
↓ ↓
[19-C] HookEvents ← [17-C-EXT] HookEnvVars │
↓ │
[19-D] SkillSystem ← [19-D-EXT] InlineCmd + Namespace + NamedArgs │
↓ ↓ │
[19-E] SessionManager ──────────────────────────────────────────────┘
↓
[19-F] OutputBudget
↓
[19-G] /init ← [22] SlashCommands (15종) ← [23] AutoCompact + isEnabled
↓
[20-A] SDK ──→ [20-B] CustomAgent ──→ [20-C] SDK 훅 콜백
↓
[21-1~7] UI 전면 개편
버전별 출시 계획 (갱신)
| 버전 | 코드명 | 포함 Phase | 핵심 신규 클래스 수 |
|---|---|---|---|
| v1.8.0 | 에이전트 인프라 | 17-UI, 17-A~G | ~35개 신규 클래스 ✅ 완료 |
| v1.8.1 | 인프라 안정화 | 버그픽스 + 17-* 마이너 | — |
| v2.0 | 에이전트 팀 | 18-A~C, L3 | ~25개 신규 클래스 ✅ 완료 |
| v2.1 | CC 완전 동등성 | 19-A~G + 19-EXT + 22 + 23 | ~50개 신규 클래스 |
| v2.2 | SDK 프로토콜 | 20-A~C (임베딩 + 외부 연동) | ~15개 신규 클래스 |
| v2.3 | AX Agent UI 전면 개편 | 21 (Claude.ai+Codex 레이아웃) | UI 계층 60% 재작성 |
| v3.0 | 크로스플랫폼 | LP-1~3 (Avalonia) | UI 계층 40% 재작성 |
v2.1 세부 구현 순서 (권장)
Week 1-2: [19-A] 4-layer Memory + [19-B + EXT] Permission + CompoundParsing
Week 3: [17-C-EXT] HookEnvVars + [19-C] HookEvents 완성
Week 4: [19-D-EXT] Skill InlineCmd + Namespace + NamedArgs
Week 5: [22] SlashCommands 16종 (ISlashCommand + Registry + 구현체)
Week 6: [19-E] SessionManager (fork/resume/tag)
Week 7: [19-F] OutputBudget + Spillover + [19-G] /init
Week 8: [23] AutoCompact + isEnabled + 최종 검증
Phase 24 — 스킬 고급 기능 (v2.1) ✅ 완료
목표: CC 스킬 시스템의 나머지 갭 해소 — 네임스페이싱, 고급 프론트매터, 실행 격리.
구현 완료 (2026-04-03):
24-A: 콜론 네임스페이싱
SkillService.LoadSkillsRecursive()— 하위 디렉토리를 재귀 탐색,database/migrate/SKILL.md→database:migrate자동 변환- 중간 디렉토리(SKILL.md 없음)는 건너뛰고 더 깊이 탐색
24-B: 고급 프론트매터 필드
context: "fork"이면 격리 서브에이전트에서 실행 (플래그 설정, 실행은 AgentLoopService에서 처리)model: 스킬별 모델 오버라이드user-invocable: false면/자동완성에서 숨김 (AI만 사용)paths: 파일 경로 글로브 — 매칭 파일 터치 시 자동 제안hooks: 스킬 스코프 훅 정의 (JSON)arguments: 명명된 인수 리스트 ([$name, $dir])when_to_use: AI 선제적 사용 힌트version: 스킬 버전
24-C: SkillDefinition 확장
- 10개 새 프로퍼티 추가 (
Context,ModelOverride,UserInvocable,Paths,ScopedHooks,Arguments,WhenToUse,Version,IsForkContext) ParseSkillFile()2-param 오버로드 (네임스페이스 이름 주입)MatchSlashCommand()—UserInvocable필터 적용PrepareSkillBodyAsync()—Arguments필드 연동
Phase 25 — 훅 고급 기능 + 도구 결과 크기 제한 (v2.1) ✅ 완료
목표: 훅 조건부 실행, asyncRewake, 누락 이벤트 추가. 도구 결과 오버플로우 방지.
구현 완료 (2026-04-03):
25-A: Hook if 조건부 실행
EvaluateCondition()완전 구현 —"field == value","field != value","field contains value"지원&&연결로 복합 조건 지원- 지원 필드:
tool_name,event,session_id,work_folder,user_message,changed_file
25-B: Hook asyncRewake + 이벤트 확장
ExtendedHookEntry.AsyncRewake필드 추가- asyncRewake 훅: 비동기 실행 후 exit code 2 시
AsyncRewakeTriggered이벤트 발생 AsyncRewakeEventArgs— HookName, AdditionalContext, SessionIdHookEventKind확장:PostToolUseFailure,Stop추가 (총 17개, CC 동등)
25-C: 도구 결과 크기 제한 (ToolResultSizer)
ToolResultSizer.Apply()— 50,000자 초과 시 임시 파일 저장 + head/tail 프리뷰 반환ToolResultSizeInfo— Output, WasTruncated, SpilloverFilePath, OriginalLengthCleanupOldResults()— 24시간 지난 임시 결과 자동 정리- CC의
maxResultSizeChars+ temp file spillover 패턴 완전 재현
Phase 26 — /init 프로젝트 초기화 (v2.1) ✅ 완료
목표: CC의
/init명령 동등 — 코드베이스 분석 → AX.md 자동 생성.구현 완료 (2026-04-03):
InitCommand (/init)
- 프로젝트 구조 트리 (3레벨, node_modules/.git 등 제외)
- Git 정보 수집 (브랜치, 리모트, 최근 커밋)
- 기술 스택 자동 감지 (Node.js/TypeScript/.NET/Rust/Go/Python/Java/Docker/GitHub Actions)
- 주요 설정 파일 내용 수집 (package.json, *.csproj, go.mod 등)
- LLM에게 프로젝트 분석 → AX.md 5섹션 자동 생성
- 프로젝트 개요, 코드 컨벤션, 빌드/테스트, 작업 가이드라인, 주의사항
- 기존 AX.md 존재 시 덮어쓰기 방지
Phase 27 — 미통합 인프라 연결 (v2.1) ✅ 완료
목표: 이미 정의되었으나 미사용 상태인 인프라(ToolResultSizer, PathBasedSkillActivator, AxMdIncludeResolver)를 실제 코드에 통합.
구현 완료 (2026-04-03):
27-A: ToolResultSizer → AgentLoopService 통합
AgentLoopService.cs:870— 기존TruncateOutput(result.Output, 4000)→ToolResultSizer.Apply(result.Output, call.ToolName)교체- 50,000자 초과 결과 → 임시 파일 + head/tail 프리뷰 → LLM에 전달
27-B: PathBasedSkillActivator → 시스템 프롬프트 통합
ChatWindow.xaml.cs—BuildPathBasedSkillSection()추가- 최근 5개 메시지에서 파일 경로 패턴 추출 →
PathBasedSkillActivator.GetActiveSkillsForFile()→ 시스템 프롬프트 자동 주입 SkillDefinitionExtensions.GetPathPatterns()— Phase 24의Paths프로퍼티 우선 사용, ExtensionStore 폴백
27-C: AxMdIncludeResolver → AX.md 로딩 통합
LoadProjectContext()— AX.md 내용에@포함 시AxMdIncludeResolver.ResolveAsync()실행@./relative/path,@~/home,@/absolute5레벨 깊이 재귀 해석- 순환 참조 감지, 파일 미존재 시 HTML 주석으로 대체
Phase 28 — 세션 매니저 (v2.1) ✅ 완료
목표: CC의 세션 관리(save/resume/fork/tag)와 동등한 세션 영속성 시스템.
구현 완료 (2026-04-03):
SessionManager
SaveSessionAsync()/LoadSessionAsync()— JSON 기반 세션 저장/복원ForkSessionAsync()— 기존 세션에서 분기 (메시지 복사 + "forked" 태그 + ParentSessionId)TagSessionAsync()/RenameSessionAsync()— 태그/이름 관리ListSessions()— 최신순 목록 (최대 50개)FindByTag()— 태그 기반 검색GetMostRecent()— CC의--continue동등 (탭별 최근 세션)CleanupOldSessions()— 30일 보존 기간 경과 시 자동 삭제AgentSession— Id, Name, Tab, WorkFolder, Model, Messages, Tags, ParentSessionId, CreatedAt/UpdatedAtAgentSessionSummary— 목록 표시용 경량 DTO
Phase 29 — 최종 통합 & 검증 (v2.1) ✅ 완료
목표: Phase 22~28에서 생성된 클래스들을 실제 코드 경로에 연결하고, CC 기능 매트릭스를 검증.
구현 완료 (2026-04-03):
29-A: AutoCompactMonitor → AgentLoopService 통합
AgentLoopService.RunAsync메인 루프에서LastTokenUsage기반 컨텍스트 사용량 모니터링AutoCompactThreshold(기본 80%) 초과 시ContextCondenser.CondenseIfNeededAsync()자동 실행AppSettings.LlmSettings.AutoCompactThreshold설정 추가
29-B: IConditionalTool + ToolEnvironmentContext 통합
AgentLoopServiceline 339:ToolEnvironmentContext빌드 후GetActiveTools(disabled, env)오버로드 사용.git폴더 존재 여부로HasGitRepo자동 감지GitTool→IConditionalTool구현 (HasGitRepo=false 시 도구 목록에서 자동 제외)
29-C: SessionManager → AgentLoopService 통합
RunAsyncfinally 블록에서 세션 자동 저장 (SessionManager.SaveSessionAsync)- 사용자 질의 첫 40자를 세션 이름으로 자동 설정
- 현재 탭, 모델, 작업 폴더, 메시지를
AgentSession으로 영속화
29-D: CC 기능 매트릭스 상태 갱신
- 33개 기능 항목 중 29개 ✅ 구현 완료, 4개 📋 계획 (SDK, 멀티에이전트, 에이전트 메모리, 3-Pane UI)
- Phase 22~29 구현 결과 반영
Phase 30 — 아키텍처 정제 & 계층 메모리 (v2.1) ✅ 완료
목표: DIP 원칙 적용, CC 4-layer 계층 메모리 통합, 기존 구현 누락 항목 매트릭스 반영.
구현 완료 (2026-04-03):
30-A: CC 기능 매트릭스 재갱신
- 기존 멀티에이전트(SubAgentTool, DelegateAgentTool, WorktreeManager) 및 에이전트 메모리(AgentTypeMemoryRepository, MemoryTool, /memory) 확인 → ✅ 반영
- 35개 항목 중 33개 ✅ 구현 완료, 2개 📋 계획 (SDK, 3-Pane UI)
30-B: IAgentMemoryRepository 인터페이스 추출
IAgentMemoryRepository인터페이스 신규 — LoadMemoryAsync, SaveMemoryAsync, AppendLearnAsync, GetAgentTypes, DeleteMemoryAgentTypeMemoryRepository→IAgentMemoryRepository구현DelegateAgentTool→ 구체 클래스 대신IAgentMemoryRepository인터페이스에 의존 (DIP)
30-C: HierarchicalMemoryService — 4-layer 메모리 통합 조회
- 4단계 우선순위: Managed(%PROGRAMDATA%) → User(%APPDATA%) → Project(WorkFolder/AX.md + .ax/rules/) → Local(AX.local.md)
CollectAll()— 모든 레벨의 MemoryFile 수집BuildMergedContext()— 단일 문자열 병합 (maxChars 제한, 시스템 프롬프트 삽입용)GetByLevel()— 특정 레벨만 필터 조회ChatWindow.LoadProjectContext()→ HierarchicalMemoryService 우선 사용, 실패 시 레거시 폴백
30-D: DelegateAgentTool에 IConditionalTool 적용
- AI 비활성화(
AiEnabled=false) 시 delegate 도구 자동 비활성화
Phase 31 — SDK 제어 프로토콜 (v2.2) ✅ 완료
목표: CC의 JSON 스트리밍 SDK와 동등한 프로그래밍 방식 에이전트 제어 프로토콜. 외부 IDE, 스크립트, CI/CD에서 AX Agent를 stdin/stdout JSON으로 제어.
구현 완료 (2026-04-03):
31-A: SDK 메시지 타입 정의 (Services/Agent/Sdk/SdkProtocol.cs)
SdkControlRequestType— 9종: Initialize, UserMessage, SetPermissionMode, SetModel, Interrupt, GetContextUsage, RewindFiles, CanUseToolResponse, TerminateSdkControlRequest— Host → CLI 요청 (Type + Id + Payload)SdkMessageType— 11종: InitializeResult, AssistantToken, AssistantMessage, ToolUseStart, ToolProgress, ToolResult, SessionResult, HookEvent, CanUseTool, Notification, ErrorSdkMessage— CLI → Host 응답/이벤트- 데이터 클래스: SdkInitializePayload, SdkUserMessagePayload, SdkTokenData, SdkToolUseStartData, SdkToolResultData, SdkSessionResultData, SdkCanUseToolData, SdkErrorData
31-B: AgentSdkServer (Services/Agent/Sdk/AgentSdkServer.cs)
- stdin/stdout 기반 양방향 JSON 스트리밍 서버
- Initialize → 설정 적용 + 커스텀 에이전트 등록 + 훅 콜백 등록
- UserMessage → AgentLoopService.RunAsync 실행 + 스트리밍 이벤트 전달
- SetModel/SetPermissionMode → 런타임 설정 변경
- Interrupt → 현재 턴 취소
- GetContextUsage → 토큰 사용량 조회
- AgentEvent → SdkMessage 자동 변환 (이벤트 구독)
- Thread-safe 출력 (SemaphoreSlim)
31-C: CustomAgentDefinition + AgentTypeRegistry (Services/Agent/Sdk/AgentTypeRegistry.cs)
CustomAgentDefinition— name, description, prompt, model, allowedTools, disallowedTools, maxTurns, permissionModeAgentTypeRegistry— CRUD + 목록 조회, Initialize 시 등록
31-D: SdkHookCallbackHandler (Services/Agent/Sdk/SdkHookCallbackHandler.cs)
- 도구 사용 허가 요청 → Host로 CanUseTool 전송 → 30초 타임아웃 대기 → Allow/Deny
- 훅 이벤트 알림 → 등록된 이벤트만 Host에 전달
- TaskCompletionSource 기반 비동기 응답 대기
Phase 32 — 3-Pane Claude.ai UI 기반 구조 (v2.3) ✅ 완료
목표: CC의 Claude.ai + Codex 레이아웃과 동등한 3-Pane UI 구조를 수립. 새 UserControl 컴포넌트 4종 + ChatWindow 통합.
구현 완료 (2026-04-03):
32-A: AgentSettingsPanel (Views/Controls/AgentSettingsPanel.xaml/.cs)
- 우측 슬라이드인 설정 패널 (TranslateTransform X 애니메이션 200ms/150ms)
- 모델 & 서비스: LLM 서비스 선택, 모델 선택, API 엔드포인트 입력
- 에이전트 동작: 최대 반복 횟수(Slider), 오류 재시도(Slider), 병렬 도구 실행(Toggle)
- 탭 전용 설정: Cowork(검증 강제) / Code(LSP, 코드 검증) 자동 분기
- 도구 관리: 등록된 모든 도구의 개별 Toggle 동적 생성
- 고급: 자동 컴팩션 임계치(Slider), 프로젝트 규칙, 개발자 모드
- 설정 변경 즉시 저장 (
SettingsService.Save())
32-B: AgentSidebarView (Views/Controls/AgentSidebarView.xaml/.cs)
- Claude.ai 스타일 좌측 사이드바 (260px, 접기 시 48px 애니메이션)
- 탭 세그먼트 (Chat|Cowork|Code) — AccentColor 배경 강조
- 새 대화 버튼, 검색 필드
- 날짜 그룹별 대화 이력 (오늘/어제/이전 7일/30일)
- 호버 효과, 대화 선택/삭제 이벤트
32-C: AgentSessionHeaderBar (Views/Controls/AgentSessionHeaderBar.xaml/.cs)
- Codex 스타일 세션 헤더 바 (42px)
- 모델 칩 (클릭 → 모델 변경), Plan 칩 (3-state 순환), 권한 칩 (4-state 순환)
- 설정 패널 토글 버튼
- 모든 칩: Border + MouseLeftButtonUp + 호버 효과 (CLAUDE.md 원칙 준수)
32-D: AgentInputArea (Views/Controls/AgentInputArea.xaml/.cs)
- Claude.ai 스타일 입력 영역
- 상단 툴바: @ 멘션 / / 스킬 / 첨부 버튼
- 멀티라인 TextBox (40~200px 자동 확장, Ctrl+Enter 전송)
- 하단 칩 열: 모델명, 권한, Plan 상태 표시
- 전송/중단 버튼 전환 (스트리밍 상태 기반)
- 첨부 파일 칩 (추가/제거)
32-E: ChatWindow.xaml 3-Pane 통합
- Grid Column 5 추가 (
SettingsPanelColumn, Auto) AgentSettingsPanel배치 — Shift+설정 버튼 클릭으로 토글xmlns:ctrl네임스페이스 등록- 기존 UI 완전 호환 유지 (기존 사이드바/메인 영역 변경 없음)
Phase 33 — 코드 품질 리팩터링 (v2.3) ✅ 완료
목표: Claude Code 동등 수준의 코드 품질 달성. 기능 파라티가 아닌 구조적 품질 파라티. AgentLoopService (925줄 RunAsync, 매직 넘버, 레이스 컨디션) + ChatWindow.xaml.cs (176건 브러시 중복, 46건 폰트 중복) 정리.
33-A: AgentLoopService — 매직 넘버 상수화
Defaults내부 정적 클래스 신설 (16개 상수)- MaxIterations, MaxRetryOnError, MaxTestFixIterations, MaxPlanRegenerationRetries 등
- ToolCallThresholdForExpansion, ContextCompressionTargetRatio, AutoCompactThresholdPercent
- QueryNameMaxLength, QueryTitleMaxLength, ThinkingTextMaxLength, VerificationSummaryMaxLength 등
33-B: AgentLoopService — RunAsync 분해 (ProcessSingleToolCallAsync 추출)
ProcessSingleToolCallAsync()메서드로 단일 도구 실행 로직 약 250줄 추출ToolExecutionState클래스: 루프 상태 공유 (카운터, 통계, 플랜 메타데이터)ToolCallActionenum: Continue/Break/Return 루프 제어RunToolHooksAsync()헬퍼: Pre/Post 훅 실행 통합 (중복 코드 제거)- RunAsync: 925줄 → 약 680줄 (약 27% 감소)
33-C: AutoCompactMonitor + ContextCondenser 통합
- 기존: 별도 실행되어 이중 압축 가능성
- 통합:
!condensed가드로 단일 흐름 보장 - 1단계: ContextCondenser 기본 압축 → 2단계: AutoCompactMonitor 임계치 기반 적극적 압축
33-D: SessionManager DI 기반 개선
- 기존:
new SessionManager()+_ = SaveSessionAsync()fire-and-forget - 개선: 생성자 주입 (
SessionManager? sessionManager = null) +await+ 에러 로깅
33-E: ChatWindow — ThemeResourceHelper 통합 적용
ThemeResourceHelper정적 헬퍼 (Phase 32에서 생성) 실제 적용TryFindResource("XXX") as Brush ?? Brushes.YYY패턴 ~162건 →ThemeResourceHelper.Primary(this)등으로 교체new FontFamily("Segoe MDL2 Assets")46건 →ThemeResourceHelper.SegoeMdl2캐시 참조로 교체
33-F: ActiveTab 레이스 컨디션 + 에러 핸들링 정리
activeTabSnapshot캡처 — RunAsync 진입 시 스냅샷, 루프 전체에서 일관 사용BuildContext(tabOverride)— 스냅샷 기반 컨텍스트 생성- RunPostToolVerificationAsync:
ActiveTab→context.ActiveTab참조 - ExecuteToolsInParallelAsync: 감사 로그
ActiveTab→context.ActiveTab - bare
catch {}5건 →catch (Exception ex) { LogService.Warn(...) }패턴으로 교체 - bare
catch { break; }→ 오류 메시지 로깅 추가
Phase 34 — ChatWindow God Class 분해 (v2.3) ✅ 완료
목표: 10,372줄 ChatWindow.xaml.cs의 구조적 개선. 중복 제거, 대형 메서드 분해, 헬퍼 추출.
34-A: SendMessageAsync Cowork/Code 중복 제거
- 기존: Cowork(35줄)과 Code(35줄) 블록이 거의 동일 — DRY 위반
RunAgentLoopAsync(tab, sendMessages, ct)단일 메서드로 통합 (~70줄 → ~40줄)- 탭별 시스템 프롬프트 분기, 완료 알림 탭별 라벨 통합
34-B: ThemeResourceHelper 완전 적용
- 잔여 14건
TryFindResource교체 (HintBackground, HintText, SeparatorColor) - ThemeResourceHelper에
Hint(),HintFg(),Separator()메서드 추가 - TryFindResource 잔여: 0건 (전량 ThemeResourceHelper로 이전 완료)
new FontFamily("Consolas")8건 →ThemeResourceHelper.Consolas캐시 참조
34-C: PopupMenuHelper 정적 헬퍼 생성 (Views/PopupMenuHelper.cs)
- ChatWindow에서 6회 이상 반복되는
Popup + Border + StackPanel + DropShadow패턴 통합 Create(target, owner, ...)— 테마 적용된 팝업 메뉴 구조 생성MenuItem(text, fg, hoverBg, onClick, isChecked, icon)— 메뉴 항목 생성Separator(),SectionHeader()— 구분선/섹션 제목- 기존 팝업 메서드들의 점진적 마이그레이션 기반 확보
34-D: AgentLoopService 잔여 정리
- 마지막 bare
catch {}→catch (Exception)+ 주석 명시 (document_plan JSON 파싱)
Phase 35 — 코드 품질 심층 정리 (v2.3) ✅ 완료
목표: 전체 코드베이스의 bare catch, 하드코딩 색상, 서비스 로케이터, 체인 액세스 패턴을 체계적으로 정리.
35-A: Bare catch 전량 정리 (109개 파일)
catch { }및catch { return X; }등 모든 bare catch →catch (Exception)+ 상황별 주석- 전체 코드베이스 109개 .cs 파일에 PowerShell 일괄 치환 적용
- 빈 catch, return 포함 catch, 값 할당 catch 등 모든 변형 포함
35-B: ColorConverter.ConvertFromString 헬퍼 추출
ThemeResourceHelper.HexBrush(hex)/HexColor(hex)정적 메서드 추가- ChatWindow.xaml.cs 47건
new SolidColorBrush((Color)ColorConverter.ConvertFromString(...))→ThemeResourceHelper.HexBrush(...)일괄 치환 - SettingsWindow, ShortcutHelpWindow, HelpDetailWindow, StatisticsViewModel 등 7개 추가 파일 34건 치환
- 총 81건 장황한 색상 변환 패턴 제거
35-C: ChatWindow _settings.Settings.Llm 체인 액세스 캐싱
private LlmSettings Llm => _settings.Settings.Llm;편의 프로퍼티 추가- 92개
_settings.Settings.Llm.체인 →Llm.단축 치환 - 가독성 향상 + 향후 리팩터링 시 단일 변경 지점 확보
35-D: AgentContext.Settings 주입 — 서비스 로케이터 제거
AgentContext에Settings(AppSettings?) +Llm(LlmSettings?) 프로퍼티 추가AgentLoopService.BuildContext()에서 Settings 주입- 에이전트 도구 11개 파일의
Application.Current as App서비스 로케이터 패턴 제거:- BuildRunTool, CodeSearchTool, CodeReviewTool, MemoryTool, DocumentPlannerTool, HttpTool, LspTool, SkillManagerTool, SubAgentTool, SnippetRunnerTool, TestLoopTool
context.Llm?.X패턴으로 교체 — DIP(의존성 역전 원칙) 준수
Phase 36 — 서비스 로케이터 제거 + PopupMenuHelper 적용 (v2.3) ✅ 완료
목표:
Application.Current as App서비스 로케이터 패턴을 전체 코드베이스에서 제거하고, PopupMenuHelper를 실제 팝업 생성 코드에 적용.
36-A: Application.Current as App → CurrentApp 정적 프로퍼티 일원화
- SettingsWindow (17건), ChatWindow (4건), 기타 12개 파일에
private static App? CurrentApp프로퍼티 추가 - 모든 인라인
Application.Current as App호출을CurrentApp으로 교체 - ChatWindow의 설정 접근은 기존
Llm프로퍼티 +_settings.Save()패턴으로 직접 대체 - 39건 → 0건 (프로퍼티 정의 제외) 서비스 로케이터 인라인 호출 제거
36-B: PopupMenuHelper 실제 적용 (ChatWindow 3개 팝업)
BtnCategoryDrop_Click—Popup+Border+StackPanel+DropShadow30줄 →PopupMenuHelper.Create()2줄ShowConversationMenu— 팝업 구조 +CreateMenuItem로컬 함수 80줄 →PopupMenuHelper.Create()+PopupMenuHelper.MenuItem()래핑 10줄ShowFileTreeContextMenu— 팝업 구조 +AddItem/AddSep로컬 함수 60줄 →PopupMenuHelper대체 5줄- 모델 선택 팝업 (
BtnModelSelector) — 팝업 구조 35줄 →PopupMenuHelper.Create()1줄 (커스텀 애니메이션 항목은 유지) - ChatWindow: 10,353줄 → 10,184줄 (169줄 감소)
Phase 37 — ChatWindow God Class 파셜 클래스 분할 (v2.3) ✅ 완료
목표: 10,184줄 ChatWindow.xaml.cs를 7개 파셜 클래스 파일로 분할하여 메인 파일을 4,767줄로 축소 (53.2% 감소).
분할 결과
| # | 파일명 | 줄 수 | 추출 내용 |
|---|---|---|---|
| 1 | ChatWindow.MessageRendering.cs |
522 | 메시지 렌더링, 체크 아이콘, 액션 버튼 |
| 2 | ChatWindow.SlashCommands.cs |
579 | 슬래시 명령 팝업, 드래그앤드롭 AI 액션 |
| 3 | ChatWindow.AgentSupport.cs |
475 | 에이전트 루프, 시스템 프롬프트, 워크플로우 |
| 4 | ChatWindow.TaskDecomposition.cs |
1,170 | Plan UI, Diff 뷰, 진행률, 이벤트 배너 |
| 5 | ChatWindow.Presets.cs |
1,280 | 주제 버튼, 프리셋 관리, 하단바, 설정 토글 |
| 6 | ChatWindow.ModelSelector.cs |
395 | 모델 선택, 프롬프트 템플릿, 대화 관리 |
| 7 | ChatWindow.PreviewAndFiles.cs |
1,105 | 미리보기 패널, 진행률 바, 파일 탐색기 |
| — | ChatWindow.xaml.cs (메인) | 4,767 | 생성자, 탭 전환, 전송, 대화 목록, 권한 등 |
핵심 성과
- 메인 파일: 10,184줄 → 4,767줄 (53.2% 감소)
- 총 코드: 10,293줄 (리다이렉트 주석 + 파셜 파일 헤더 오버헤드 ~1%)
- 빌드: 경고 0, 오류 0
- 각 파셜 파일: 독립적 섹션으로 응집도 높은 메서드 그룹
- 필드 이동: 각 섹션에 속하는 필드를 함께 이동하여 관련 코드 근접 배치
Phase 38 — SettingsWindow 파셜 클래스 분할 (v2.3) ✅ 완료
목표: 3,216줄 SettingsWindow.xaml.cs를 3개 파셜 클래스 파일로 분할.
| 파일 | 줄 수 | 내용 |
|---|---|---|
SettingsWindow.xaml.cs (메인) |
373 | 생성자, 필드, 저장/닫기, 스니펫 이벤트 |
SettingsWindow.UI.cs |
802 | 섹션 헬퍼, 탭 전환, 독바, 스토리지, 핫키, 버전 |
SettingsWindow.Tools.cs |
875 | 도구/커넥터 카드 UI, AX Agent 탭, 도구 관리 |
SettingsWindow.AgentConfig.cs |
1,202 | 모델 등록, 스킬, 템플릿, AI토글, 네트워크모드, 훅, MCP |
- 메인 파일: 3,216줄 → 373줄 (88.4% 감소)
- 빌드: 경고 0, 오류 0
Phase 39 — FontFamily 캐싱 + LauncherWindow 파셜 분할 (v2.3) ✅ 완료
목표: 89개
new FontFamily(...)반복 생성 제거 + LauncherWindow 파셜 분할.
FontFamily 캐싱 (25개 파일)
ThemeResourceHelper에 5개 정적 필드 추가:
SegoeMdl2—new FontFamily("Segoe MDL2 Assets")(기존)Consolas—new FontFamily("Consolas")(기존)CascadiaCode—new FontFamily("Cascadia Code, Consolas, monospace")(신규)ConsolasCode—new FontFamily("Consolas, Cascadia Code, Segoe UI")(신규)ConsolasCourierNew—new FontFamily("Consolas, Courier New")(신규)
총 89개 new FontFamily(...) 호출 → 정적 캐시 필드 참조로 교체 (25개 파일)
LauncherWindow 파셜 분할
| 파일 | 줄 수 | 내용 |
|---|---|---|
LauncherWindow.xaml.cs (메인) |
578 | Win32 P/Invoke, 생성자, Show(), 아이콘 20종 애니메이션 |
LauncherWindow.Theme.cs |
116 | ApplyTheme, 테마 빌드, BuildCustomDictionary, IsSystemDarkMode |
LauncherWindow.Animations.cs |
153 | 무지개 글로우, 애니메이션 헬퍼, CenterOnScreen, AnimateIn |
LauncherWindow.Keyboard.cs |
593 | IME 검색, PreviewKeyDown, KeyDown 20여 단축키, ShowToast |
LauncherWindow.Shell.cs |
177 | Shell32 P/Invoke, SendToRecycleBin, ShowLargeType, 클릭 핸들러 |
- 메인 파일: 1,563줄 → 578줄 (63.0% 감소)
- 빌드: 경고 0, 오류 0
Phase 40 — ChatWindow 2차 파셜 클래스 분할 (v2.3) ✅ 완료
목표: 4,767줄 ChatWindow.xaml.cs (1차 분할 후 잔여)를 7개 파셜 파일로 추가 분할.
| 파일 | 줄 수 | 내용 |
|---|---|---|
ChatWindow.xaml.cs (메인) |
262 | 필드, 생성자, OnClosing, ForceClose, ConversationMeta |
ChatWindow.Controls.cs |
595 | 사용자 정보, 스크롤, 제목 편집, 카테고리 드롭다운, 탭 전환 |
ChatWindow.WorkFolder.cs |
359 | 작업 폴더 메뉴, 폴더 설정, 컨텍스트 메뉴 |
ChatWindow.PermissionMenu.cs |
498 | 권한 팝업, 데이터 활용 메뉴, 파일 첨부, 사이드바 토글 |
ChatWindow.ConversationList.cs |
747 | 대화 목록, 그룹 헤더, 제목 편집, 검색, 날짜 포맷 |
ChatWindow.Sending.cs |
720 | 편집 모드, 타이머, SendMessageAsync, BtnSend_Click |
ChatWindow.HelpCommands.cs |
157 | /help 도움말 창, AddHelpSection |
ChatWindow.ResponseHandling.cs |
1,494 | 응답재생성, 스트리밍, 내보내기, 팁, 토스트, 상태바, 키보드 |
- 메인 파일: 4,767줄 → 262줄 (94.5% 감소)
- 전체 ChatWindow 파셜 파일 수: 15개 (1차 7개 + 2차 7개 + 메인 1개)
- 빌드: 경고 0, 오류 0
Phase 41 — SettingsViewModel·AgentLoopService 파셜 분할 (v2.3) ✅ 완료
목표: SettingsViewModel (1,855줄)·AgentLoopService (1,823줄) 파셜 클래스 분할.
SettingsViewModel 분할
| 파일 | 줄 수 | 내용 |
|---|---|---|
SettingsViewModel.cs (메인) |
320 | 클래스 선언, 필드, 이벤트, 생성자 |
SettingsViewModel.Properties.cs |
837 | 바인딩 프로퍼티 전체 (LLM, 런처, 기능토글, 테마 등) |
SettingsViewModel.Methods.cs |
469 | Save, Browse, AddShortcut, AddSnippet 등 메서드 |
SettingsViewModelModels.cs |
265 | ThemeCardModel, ColorRowModel, SnippetRowModel 등 6개 모델 클래스 |
- 메인 파일: 1,855줄 → 320줄 (82.7% 감소)
AgentLoopService 분할
| 파일 | 줄 수 | 내용 |
|---|---|---|
AgentLoopService.cs (메인) |
1,334 | 상수, 필드, 생성자, RunAsync 루프, 헬퍼 |
AgentLoopService.Execution.cs |
498 | TruncateOutput, ReadOnlyTools, ClassifyToolCalls, ToolExecutionState, ParallelState, ExecuteToolsInParallelAsync |
- 메인 파일: 1,823줄 → 1,334줄 (26.8% 감소)
- 빌드: 경고 0, 오류 0
Phase 42 — ChatWindow.ResponseHandling·LlmService 파셜 분할 (v2.3) ✅ 완료
목표: ChatWindow.ResponseHandling (1,494줄)·LlmService (1,010줄) 추가 분할.
ChatWindow.ResponseHandling 분할
| 파일 | 줄 수 | 내용 |
|---|---|---|
ChatWindow.ResponseHandling.cs |
741 | 응답 재생성, 스트리밍 완료 마크다운, 중지, 대화 분기, 팔레트 |
ChatWindow.MessageActions.cs |
277 | 버튼 이벤트, 메시지 내 검색(Ctrl+F), 에러 복구 |
ChatWindow.StatusAndUI.cs |
498 | 우클릭 메뉴, 팁, AX.md, 무지개 글로우, 토스트, 하단바, 헬퍼 |
- 원본 대비: 1,494줄 → 741줄 (50.3% 감소)
LlmService 분할
| 파일 | 줄 수 | 내용 |
|---|---|---|
LlmService.cs (메인) |
263 | 필드, 생성자, 라우팅, 시스템 프롬프트, 비스트리밍 |
LlmService.Streaming.cs |
516 | StreamAsync, TestConnectionAsync, 백엔드별 구현 |
LlmService.Helpers.cs |
252 | 메시지 빌드, HTTP 재시도, 토큰 파싱, Dispose |
- 메인 파일: 1,010줄 → 263줄 (74.0% 감소)
- 빌드: 경고 0, 오류 0
Phase 43 — 4개 대형 파일 파셜 분할 (v2.3) ✅ 완료
목표: SettingsWindow.AgentConfig·ChatWindow.Presets·PreviewAndFiles·WorkflowAnalyzerWindow 동시 분할.
| 원본 파일 | 원본 | 메인 | 신규 파일 | 신규 줄 수 |
|---|---|---|---|---|
| SettingsWindow.AgentConfig.cs | 1,202 | 608 | AgentHooks.cs | 605 |
| ChatWindow.Presets.cs | 1,280 | 315 | CustomPresets.cs | 978 |
| ChatWindow.PreviewAndFiles.cs | 1,105 | 709 | FileBrowser.cs | 408 |
| WorkflowAnalyzerWindow.xaml.cs | 929 | 274 | Charts.cs | 667 |
- 총 신규 파일: 4개
- 빌드: 경고 0, 오류 0
Phase 44 — LauncherViewModel·SettingsWindow.Tools·MarkdownRenderer 파셜 분할 (v2.3) ✅ 완료
목표: LauncherViewModel(805줄)·SettingsWindow.Tools(875줄)·MarkdownRenderer(825줄) 분할.
| 원본 파일 | 원본 | 메인 | 신규 파일 | 신규 줄 수 |
|---|---|---|---|---|
| LauncherViewModel.cs | 805 | 402 | FileAction.cs | 154 |
| LauncherViewModel.cs | — | — | Commands.cs | 273 |
| SettingsWindow.Tools.cs | 875 | 605 | ToolCards.cs | 283 |
| MarkdownRenderer.cs | 825 | 621 | Highlighting.cs | 215 |
- LauncherViewModel 메인: 805줄 → 402줄 (50.1% 감소)
- 총 신규 파일: 4개
- 빌드: 경고 0, 오류 0
Phase 45 — AppSettings.cs 클래스 파일 분리 (v2.3) ✅ 완료
목표: 31개 클래스가 혼재된 AppSettings.cs (1,320줄)를 3개 파일로 분리.
| 파일 | 줄 수 | 내용 |
|---|---|---|
AppSettings.cs |
564 | AppSettings·LauncherSettings·CustomThemeColors 등 17개 클래스 |
AppSettings.LlmSettings.cs |
481 | LlmSettings(408줄)·CodeSettings |
AppSettings.AgentConfig.cs |
284 | 권한·훅·이벤트·모델·프리셋 등 12개 설정 클래스 |
- 메인 파일: 1,320줄 → 564줄 (57.3% 감소)
- 빌드: 경고 0, 오류 0
Phase 46 — AgentLoopService·ChatWindow·SettingsViewModel·TemplateService·PlanViewerWindow 분리 (v2.3) ✅ 완료
목표: 6개 대형 파일을 16개 파셜 파일로 분리.
| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|---|---|---|---|
AgentLoopService.cs |
1,334 → 846 | AgentLoopService.HtmlReport.cs (151) |
HTML 자동저장, 텍스트→HTML 변환 |
AgentLoopService.Verification.cs (349) |
도구 실행 후 검증, 문서 생성 감지 | ||
ChatWindow.TaskDecomposition.cs |
1,170 → 307 | ChatWindow.PlanViewer.cs (474) |
Plan 카드, 결정 버튼, 플랜 뷰어 제어 |
ChatWindow.EventBanner.cs (411) |
에이전트 이벤트 배너, 파일 퀵액션 | ||
SettingsViewModel.Properties.cs |
837 → 427 | SettingsViewModel.LlmProperties.cs (417) |
LLM/에이전트 설정 바인딩 프로퍼티 |
TemplateService.cs |
734 → 179 | TemplateService.Css.cs (559) |
11개 CSS 스타일 상수 |
PlanViewerWindow.cs |
931 → 324 | PlanViewerWindow.StepRenderer.cs (616) |
단계 목록 렌더링, 드래그, 편집 |
- 빌드: 경고 0, 오류 0
Phase 47 — ChatWindow 추가 분리 (v2.3) ✅ 완료
목표: ChatWindow 관련 대형 파일 추가 분리.
| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|---|---|---|---|
ChatWindow.ResponseHandling.cs |
741 → 269 | ChatWindow.StreamingUI.cs (303) |
스트리밍 컨테이너, 파싱, 토큰 |
ChatWindow.ConversationExport.cs (188) |
대화 분기, 커맨드 팔레트, 내보내기 | ||
ChatWindow.PreviewAndFiles.cs |
709 → ~340 | ChatWindow.PreviewPopup.cs (~230) |
프리뷰 탭 컨텍스트 메뉴, 팝업 |
HelpDetailWindow.xaml.cs |
673 → 254 | HelpDetailWindow.Shortcuts.cs (168) |
단축키 항목 목록 빌드 |
HelpDetailWindow.Navigation.cs (266) |
테마, 상단 메뉴, 카테고리 바, 페이지 이동 | ||
SkillService.cs |
661 → 386 | SkillService.Import.cs (203) |
스킬 내보내기/가져오기/도구명 매핑 |
SkillDefinition.cs |
(신규) | SkillDefinition.cs (81) |
SkillDefinition 클래스 독립 파일 |
- 빌드: 경고 0, 오류 0
Phase 48 — ChatWindow·WorkflowAnalyzer·SkillGallery 분리 (v2.3) ✅ 완료
목표: 탭 전환, 드롭 액션, 워크플로우 타임라인, 스킬 상세 분리.
| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|---|---|---|---|
ChatWindow.Controls.cs |
595 → 372 | ChatWindow.TabSwitching.cs (232) |
탭 전환, Plan 모드 UI, 대화 복원 |
ChatWindow.SlashCommands.cs |
579 → 406 | ChatWindow.DropActions.cs (160) |
드래그&드롭 액션 팝업, 코드/데이터 확장자 |
WorkflowAnalyzerWindow.Charts.cs |
667 → 397 | WorkflowAnalyzerWindow.Timeline.cs (281) |
타임라인 노드, 배지, 상세, 요약 카드 |
SkillGalleryWindow.xaml.cs |
631 → ~430 | SkillGalleryWindow.SkillDetail.cs (197) |
스킬 상세 팝업 |
- 빌드: 경고 0, 오류 0
Phase 49 — ChatWindow.Sending·MarkdownRenderer·MessageRendering 분리 (v2.3) ✅ 완료
목표: 전송 로직, 마크다운 렌더러, 메시지 렌더링 분리.
| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|---|---|---|---|
ChatWindow.Sending.cs |
720 → ~300 | ChatWindow.MessageEdit.cs (~180) |
편집 모드 진입, SubmitEditAsync |
ChatWindow.StreamingTimers.cs (~58) |
커서/경과시간/타이핑 타이머 | ||
MarkdownRenderer.cs |
621 → 405 | MarkdownRenderer.CodeBlock.cs (218) |
코드 블록 UI, 파일저장, 전체화면 |
ChatWindow.MessageRendering.cs |
522 → 220 | ChatWindow.Animations.cs (172) |
애니메이션 헬퍼, 체크 아이콘 |
ChatWindow.FeedbackButtons.cs (121) |
액션 버튼, 좋아요/싫어요 피드백 |
- 빌드: 경고 0, 오류 0
Phase 50 — PlanViewerWindow·SettingsWindow 추가 분리 (v2.3) ✅ 완료
목표: 버튼 빌더, AI 토글, MCP 고급 설정 분리.
| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|---|---|---|---|
PlanViewerWindow.StepRenderer.cs |
616 → 425 | PlanViewerWindow.EditButtons.cs (197) |
승인/실행/닫기 버튼, 편집 입력 |
SettingsWindow.AgentConfig.cs |
608 → 303 | SettingsWindow.AiToggle.cs (316) |
AI 활성화·네트워크 모드 토글 |
SettingsWindow.AgentHooks.cs |
605 → 334 | SettingsWindow.McpAdvanced.cs (271) |
MCP 서버 관리, 폴백 모델, 고급 설정 |
- 빌드: 경고 0, 오류 0
Phase 51 — 9개 대형 파일 → 19개 파일 분리 (v2.3) ✅ 완료
목표: 핸들러·서비스·모델 대규모 분리.
| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|---|---|---|---|
SettingsWindow.Tools.cs |
605 → 238 | SettingsWindow.SkillListPanel.cs (295) |
스킬 목록 섹션, 스킬 그룹 카드 |
LauncherWindow.Keyboard.cs |
593 → 454 | LauncherWindow.ShortcutHelp.cs (139) |
단축키 도움말, 토스트, 특수 액션 |
CalculatorHandler.cs |
566 → ~240 | UnitConverter.cs (152) |
단위 변환 클래스 독립 파일 |
MathEvaluator.cs (183) |
수식 파서 클래스 독립 파일 | ||
EmojiHandler.cs |
553 → 70 | EmojiHandler.Data.cs (490) |
이모지 데이터베이스 배열 |
DocumentPlannerTool.cs |
598 → 324 | DocumentPlannerTool.Generators.cs (274) |
HTML/DOCX/Markdown 생성, 섹션 계획 |
DocumentReaderTool.cs |
571 → 338 | DocumentReaderTool.Formats.cs (233) |
BibTeX/RIS/DOCX/XLSX/Text/Helpers |
CodeIndexService.cs |
588 → 285 | CodeIndexService.Search.cs (199) |
검색, TF-IDF, 토큰화, Dispose |
ClipboardHistoryService.cs |
575 → 458 | ClipboardHistoryService.ImageCache.cs (120) |
이미지 캐시 유틸리티 |
IndexService.cs |
568 → 412 | IndexService.Helpers.cs (163) |
경로 탐색 헬퍼, 검색 캐시 |
- 빌드: 경고 0, 오류 0
Phase 52 — 7개 파일 추가 분리 (v2.3) ✅ 완료
목표: 스트리밍, 스킬, 핸들러, 모델, 뷰 분리 마무리.
| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|---|---|---|---|
LlmService.Streaming.cs |
516 → 256 | LlmService.GeminiClaude.cs (260) |
Gemini·Claude 스트리밍 메서드 |
DocxSkill.cs |
543 → 158 | DocxSkill.Builders.cs (290) |
11개 Word 문서 빌더 헬퍼 |
ChartSkill.cs |
537 → 174 | ChartSkill.Renderers.cs (280) |
7개 차트 렌더러, 헬퍼, Dataset |
ScreenCaptureHandler.cs |
637 → 241 | ScreenCaptureHandler.Helpers.cs (310) |
스크롤·영역 캡처, 이미지 처리 헬퍼 |
SystemInfoHandler.cs |
509 → 352 | SystemInfoHandler.Helpers.cs (161) |
8개 시스템 정보 헬퍼, StarInfoHandler |
AppSettings.cs |
564 → 309 | AppSettings.Models.cs (233) |
14개 설정 모델 클래스 |
SkillEditorWindow.xaml.cs |
528 → 303 | SkillEditorWindow.PreviewSave.cs (226) |
미리보기·저장·편집 모드 로드 |
PreviewWindow.xaml.cs |
505 → 317 | PreviewWindow.Content.cs (170) |
콘텐츠 로드, 타이틀바 핸들러 |
코드 품질 최종 결과 (Phase 46~52)
| 구분 | Phase 45 이전 | Phase 52 완료 후 |
|---|---|---|
| 500줄 이상 파일 | 40+ | 3개 (AgentLoopService 846줄·LauncherWindow.xaml 578줄·TemplateService.Css 559줄) |
| 허용 이유 | — | RunAsync 679줄 단일 메서드 / 345줄 애니메이션 메서드 / CSS 데이터 상수 |
| 신규 파일 수 | — | Phase 46~52 동안 45개 신규 partial 파일 생성 |
- 빌드: 경고 0, 오류 0
Phase 17-UI-A — AgentSettingsPanel 완전 통합 (v1.8.0) ✅ 완료
목표: 채팅 창 우측 AgentSettingsPanel을 주 설정 엔트리로 승격. 기어 버튼 클릭 → 인라인 패널 직접 열기 (Shift+Click 불필요). ToolRegistry 의존 제거, 탭 전환 연동 완성.
변경 파일
| 파일 | 변경 내용 |
|---|---|
ChatWindow.MoodMenu.cs |
BtnSettings_Click: Shift 조건 제거 → 직접 ToggleSettingsPanel() 호출; Ctrl+클릭만 SettingsWindow 열기 |
ChatWindow.MoodMenu.cs |
ToggleSettingsPanel(): _toolRegistry 패널에 전달; SettingsChanged 이벤트 연결 → UpdateModelLabel() + UpdateAnalyzerButtonVisibility() 반영 |
ChatWindow.MoodMenu.cs |
OnSettingsPanelChanged() 신규: 패널 설정 변경 시 ChatWindow UI 즉시 갱신 |
ChatWindow.TabSwitching.cs |
UpdateTabUI(): 설정 패널 열림 상태이면 UpdateActiveTab() 자동 호출 |
ChatWindow.xaml |
설정 버튼 ToolTip: "설정 패널 (Ctrl+클릭: 전역 설정)" |
AgentSettingsPanel.xaml.cs |
LoadFromSettings(): ToolRegistry? toolRegistry 매개변수 추가 |
AgentSettingsPanel.xaml.cs |
BuildToolToggles(): 외부 레지스트리 우선, 없으면 ToolRegistry.CreateDefault() 폴백 |
개선 효과
- ⚙ 버튼 한 번 클릭으로 에이전트 설정 패널 바로 접근
- 탭 전환(Chat/Cowork/Code) 시 패널 탭별 설정(검증 강제, LSP 등) 자동 반영
- 설정 변경 즉시 ChatWindow 모델 라벨·분석기 버튼 상태 갱신
- 도구 토글 패널: 실제 ChatWindow ToolRegistry 참조로 정확한 목록 표시
- 빌드: 경고 0, 오류 0
Phase 17-UI-B — 헤더 바 모델·권한 칩 추가 (v1.8.0) ✅ 완료
목표: 서브 헤더 바(Row 1)에 현재 모델과 권한 모드를 항상 노출. 클릭 시 각각 모델 선택기·권한 팝업을 인라인으로 열어 UX 일관성 강화.
변경 파일
| 파일 | 변경 내용 |
|---|---|
ChatWindow.xaml |
서브 바 우측에 ModelHeaderChip Button 추가: 브레인 아이콘 + ModelHeaderLabel TextBlock |
ChatWindow.xaml |
서브 바 우측에 PermissionHeaderChip Button 추가: 잠금 아이콘(#4FC3F7) + PermissionHeaderLabel TextBlock |
ChatWindow.ModelSelector.cs |
UpdateModelLabel(): ModelHeaderLabel 동기 갱신 코드 추가 |
ChatWindow.PermissionMenu.cs |
UpdatePermissionUI(): PermissionHeaderLabel 동기 갱신 코드 추가 |
ChatWindow.PermissionMenu.cs |
PermissionHeaderChip_Click() 신규: 팝업 PlacementTarget을 헤더 칩으로 교체 후 기존 권한 팝업 호출 |
ChatWindow.xaml.cs |
Loaded 핸들러: UpdatePermissionUI() 초기 호출 추가 |
개선 효과
- 서브 헤더 바에서 현재 모델(예: "Claude · Sonnet 4.6")과 권한 모드(예: "Ask") 상시 확인 가능
ModelHeaderChip클릭 → 기존 모델 선택 팝업 즉시 열림PermissionHeaderChip클릭 → 권한 팝업이 헤더 칩 기준 하단으로 올바르게 열림- 탭 전환·설정 변경 시 양쪽 칩(폴더 바 + 헤더 바) 동시 갱신
- 빌드: 경고 0, 오류 0
Phase 17-UI-C — AgentSessionHeaderBar UserControl 통합 (v1.8.0) ✅ 완료
목표: 기존 Row 1 서브 바의 모델·Plan·권한 칩을
AgentSessionHeaderBarUserControl로 교체. 투명 배경으로 기존 레이아웃에 임베드. 이벤트 기반 위임으로 ChatWindow 기존 로직 재사용.
변경 파일
| 파일 | 변경 내용 |
|---|---|
AgentSessionHeaderBar.xaml |
외부 Border를 Background="Transparent" BorderThickness="0"으로 수정. 높이 42→38px. x:Name="PlanIcon", x:Name="PermIcon" 추가. |
AgentSessionHeaderBar.xaml.cs |
SetPlanMode(): PlanIcon x:Name 직접 참조. ChipPermission_Click: 이벤트 발행만 (팝업은 ChatWindow 처리). SetPermissionMode(): PermIcon 색상 업데이트. |
ChatWindow.xaml |
Row 1 우측에서 ModelHeaderChip, BtnPlanMode, PermissionHeaderChip 제거 → <ctrl:AgentSessionHeaderBar x:Name="SessionHeaderBar"/> 추가. |
ChatWindow.SessionHeaderBar.cs |
신규: InitSessionHeaderBar() — ModelChipClicked·PlanModeChanged·PermissionModeChanged·SettingsRequested 이벤트 구독. |
ChatWindow.ModelSelector.cs |
UpdateModelLabel(): SessionHeaderBar?.SetModel() 동기화. |
ChatWindow.PermissionMenu.cs |
UpdatePermissionUI(): SessionHeaderBar?.SetPermissionMode() 동기화. |
ChatWindow.TabSwitching.cs |
UpdateTabUI(): SessionHeaderBar?.SetTabLabel() 동기화. UpdatePlanModeUI(): SessionHeaderBar 전용으로 단순화. |
ChatWindow.xaml.cs |
Loaded: InitSessionHeaderBar() 호출 추가. |
- 빌드: 경고 0, 오류 0
Phase 17-UI-D — AgentSidebarView UserControl 통합 (v1.8.0) ✅ 완료
목표: 기존
SidebarPanelBorder(인라인 XAML 110줄)를AgentSidebarViewUserControl로 교체. 34개 기존 ChatWindow 파셜 파일이 사이드바 내부 요소를 무수정으로 참조할 수 있도록 프록시 패턴 적용.
변경 파일
| 파일 | 변경 내용 |
|---|---|
AgentSidebarView.xaml |
7행 구조로 재작성: 헤더(로고+새대화) / 탭세그먼트 / 검색 / 카테고리드롭다운 / 대화목록 / 삭제 / 사용자계정. |
AgentSidebarView.xaml.cs |
완전 재작성: internal 프록시 프로퍼티 8개 + SidebarTabChanged/NewChatRequested/DeleteAllRequested/CategoryDropClicked/SearchTextChanged 이벤트. |
ChatWindow.xaml |
<Border x:Name="SidebarPanel"> 인라인 110줄 → <ctrl:AgentSidebarView x:Name="Sidebar"/> 1줄 대체. |
ChatWindow.SidebarCompat.cs |
신규: 8개 계산 프로퍼티(ConversationPanel, SearchBox, CategoryIcon, CategoryLabel, BtnCategoryDrop, UserInitialSidebar, UserNameText, UserPcText) + InitSidebarEvents(). |
ChatWindow.PermissionMenu.cs |
BtnToggleSidebar_Click(): SidebarPanel.Visibility → Sidebar.Visibility. PermissionHeaderChip_Click(): PermissionHeaderChip → SessionHeaderBar. |
ChatWindow.TabSwitching.cs |
탭 핸들러 3개: Sidebar?.SetActiveTab() 동기화 추가. UpdatePlanModeUI(): 죽은 코드 제거. |
ChatWindow.xaml.cs |
Loaded: InitSidebarEvents() 호출 추가. |
- 빌드: 경고 0, 오류 0
Phase 17-UI-E — AgentInputArea 툴바 통합 (v1.8.0) ✅ 완료
목표:
AgentInputAreaUserControl에IsToolbarOnly모드 추가. 기존 InputBox 위에 @ / 스킬 / 첨부 툴바 행을 추가하여 Claude.ai 스타일 입력 경험 제공.
변경 파일
| 파일 | 변경 내용 |
|---|---|
AgentInputArea.xaml |
x:Name="OuterBorder", x:Name="InputTextBoxRow", x:Name="ChipsSendRow" 추가. |
AgentInputArea.xaml.cs |
IsToolbarOnly 의존성 프로퍼티 추가. ApplyToolbarOnlyMode(): TextBox/칩 행 Collapsed, 외부 Border 투명화. |
ChatWindow.xaml |
InputBorder Grid에 Row 0 추가(ctrl:AgentInputArea x:Name="InputToolbar" IsToolbarOnly="True"). 기존 Row 0→1, 1→2, 2→3 시프트. |
ChatWindow.InputToolbar.cs |
신규: InitInputToolbar() — MentionRequested(@삽입), SkillRequested(/삽입), AttachRequested(BtnAttach_Click 위임). |
ChatWindow.xaml.cs |
Loaded: InitInputToolbar() 호출 추가. |
- 빌드: 경고 0, 오류 0
Phase 17-A — Reflexion 강화 (v1.8.0) ✅ 완료
목표: 성공·실패 모두 구조화된 자기평가 저장 → 동일 작업 유형 재실행 시 자동 참고
변경 파일
| 파일 | 변경 내용 |
|---|---|
ReflexionService.cs |
ReflexionEvaluatorService 신규 추가: LLM 기반 자기평가(점수·강점·약점·교훈 추출), 규칙 기반 폴백 엔트리. ReflexionRepository.BuildContextPromptAsync() maxEntries 파라미터 추가. |
AgentLoopService.Reflexion.cs (신규, 82줄) |
InjectReflexionContextAsync() — 세션 시작 시 과거 교훈을 시스템 메시지에 주입. FireAndForgetReflexionEval() — 세션 완료 후 비동기 자기평가 저장. |
AgentLoopService.cs |
RunAsync() 루프 시작 전: InjectReflexionContextAsync() 호출. finally 블록: FireAndForgetReflexionEval() 호출. |
AgentSettingsPanel.xaml |
"자기성찰 메모리" 섹션 추가: 활성화 토글, 성공 시만 평가 토글, 최대 참고 교훈 수 슬라이더. |
AgentSettingsPanel.xaml.cs |
LoadFromSettings(): Reflexion 설정 초기화. ChkReflexion_Changed(), ChkReflexionSuccessOnly_Changed(), SliderReflexionMaxEntries_ValueChanged() 핸들러 추가. |
구현 세부사항
- ReflexionEvaluatorService:
$$"""..."""raw string으로 평가 프롬프트 생성, JSON 블록 추출 후 역직렬화 - 작업 유형 분류:
TaskTypeClassifier.Classify()— 8개 카테고리 (code_generation, file_refactor, document, analysis, search, test, git, debug, general) - 저장 위치:
%APPDATA%\AxCopilot\reflexion\<taskType>.jsonl(JSONL 형식, 최신 N개 역순 조회) - 비동기 처리: fire-and-forget — 루프 취소(
CancellationToken) 영향 없음 - 빌드: 경고 0, 오류 0
Phase 17-B — 구조화된 태스크 상태 + 이벤트 로그 통합 (v1.8.0) ✅ 완료
목표: 대화 압축 시에도 유지되는 Working Memory + JSONL 이벤트 로그 완전 통합
변경 파일
| 파일 | 변경 내용 |
|---|---|
AgentLoopService.TaskState.cs (신규, 96줄) |
InitTaskStateAsync() — 세션 시작 시 TaskState 초기화·현재 작업 기록. TrackToolFile() — 도구 성공 시 파일 경로 참조 목록 추가(fire-and-forget). InjectTaskStateContext() — 압축 전 Working Memory를 시스템 메시지에 in-place 주입. UpdateTaskStateSummaryAsync() — 압축 완료 후 컨텍스트 요약 갱신(fire-and-forget). |
AgentLoopService.cs |
RunAsync(): UserMessage 이벤트 기록 + InitTaskStateAsync() 호출. 압축 블록: InjectTaskStateContext() 호출 + CompactionCompleted/CompactionTriggered 이벤트 로그. LLM 응답 후: AssistantMessage 이벤트 기록. |
AgentLoopService.Execution.cs |
도구 성공 시 TrackToolFile(result.FilePath) 호출 — 파일 경로 Working Memory 추적. |
구현 세부사항
- TaskState 지연 초기화:
_taskState ??= new TaskStateService()— 사용 시 첫 생성, 세션 간InitializeAsync(sessionId)로 상태 재로드 - In-place 주입:
InjectTaskStateContext()가## 현재 작업 상태 (Working Memory)마커로 기존 섹션 탐지 후 교체 → 중복 방지 - 이벤트 커버리지: SessionStart/End(기존), UserMessage, AssistantMessage, ToolRequest/Result(기존), CompactionTriggered, CompactionCompleted 모두 JSONL 기록
- 설정 연동:
LlmSettings.EnableTaskState+LlmSettings.EventLog.Enabled체크 후 동작 - 저장 위치:
%APPDATA%\AxCopilot\sessions\{sessionId}\task_state.json+events.jsonl - 빌드: 경고 0, 오류 0
Phase 17-C — 훅 시스템 고도화 (v1.8.0) ✅ 완료
목표: ExtendedHookRunner를 에이전트 라이프사이클 전 구간에 연결 + Prompt 모드 구현
변경 파일
| 파일 | 변경 내용 |
|---|---|
AppSettings.AgentConfig.cs |
ExtendedHooksConfig에 4개 이벤트 추가: preToolUse, postToolUse, postToolUseFailure, agentStop |
AgentLoopService.ExtendedHooks.cs (신규, 150줄) |
RunExtendedEventAsync() — 이벤트 훅 실행·결과 적용. GetExtendedHooks() — 설정에서 런타임 엔트리 조회. ConvertToRuntimeEntries() — ExtendedHookEntryConfig → ExtendedHookEntry 변환. ApplyExtendedHookResult() — additionalContext 시스템 메시지 in-place 주입. |
ExtendedHookRunner.cs |
RunEventAsync() + ExecuteSingleAsync()에 LlmService? llm 파라미터 추가. RunPromptHookAsync() 신규 구현: {{tool_name}} 등 변수 치환, LLM 호출, 차단 신호 감지. using AxCopilot.Models 추가. |
AgentLoopService.cs |
TaskState init 후: SessionStart 훅(fire-and-forget) + UserPromptSubmit 훅(차단 시 즉시 반환). 적극적 압축 직전: PreCompact 훅. 압축 완료 후: PostCompact 훅(fire-and-forget). finally 블록: SessionEnd + AgentStop 훅(fire-and-forget). |
AgentLoopService.Execution.cs |
RunToolHooksAsync() 개선: 레거시 AgentHookRunner 유지 + ExtendedHookRunner PreToolUse/PostToolUse/PostToolUseFailure 이벤트 추가 실행. |
구현 세부사항
- 이벤트 커버리지: SessionStart, SessionEnd, AgentStop, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PreCompact, PostCompact, FileChanged, PermissionRequest — 11종 이벤트 완전 연결
- 실행 모드: Command(bat/ps1), Http(POST webhook), Prompt(LLM 평가) 구현. Agent 모드는 Phase 18-A 예정.
- Prompt 모드:
{{tool_name}},{{tool_input}},{{tool_output}},{{user_message}},{{event}},{{session_id}}변수 치환. 응답에 block/deny/차단 포함 시 Block=true. - 차단 처리: UserPromptSubmit 훅 Block=true 시 즉시
"⚠ 요청이 훅 정책에 의해 차단되었습니다."반환 - HookFired 이벤트 로그: 훅 실행 시 JSONL에 eventKind, hookCount, blocked 기록
- 하위 호환: 레거시 AgentHooks(command 스크립트) 병행 유지
- 빌드: 경고 0, 오류 0
Phase 17-D — 스킬 시스템 고도화 (v1.8.0) ✅ 완료
목표: paths: glob 패턴 기반 스킬 자동 주입 + context:fork 서브에이전트 격리 실행
변경 파일
| 파일 | 변경 내용 |
|---|---|
AgentLoopService.Skills.cs (신규, 95줄) |
InjectPathBasedSkills() — 파일 경로 매칭 스킬 시스템 메시지 in-place 주입, SkillActivated 이벤트 로그. RunSkillInForkAsync() — context:fork 스킬 격리 LLM 실행, SkillCompleted 이벤트 로그. |
SkillManagerTool.cs |
SetForkRunner() 콜백 필드 추가. exec 액션 + arguments 파라미터 추가. ExecSkillAsync(): PrepareSkillBodyAsync 준비 → IsForkContext 시 fork runner 호출 → 일반 스킬은 시스템 프롬프트 반환. |
AgentLoopService.cs |
생성자: SkillManagerTool에 RunSkillInForkAsync fork runner 주입. |
AgentLoopService.Execution.cs |
도구 성공 후 InjectPathBasedSkills(result.FilePath, messages) 호출. |
구현 세부사항
- paths: 자동 주입: 파일 도구(
file_read,file_write등) 성공 후result.FilePath로 GlobMatcher 매칭 → 해당 스킬의 시스템 프롬프트를 메시지에 자동 주입.## 현재 파일에 자동 적용된 스킬마커로 in-place 교체. - context:fork:
SkillManagerTool.exec호출 시skill.IsForkContext == true이면 격리된 LLM 컨텍스트(도구 없음)에서 실행. 결과를[Fork 스킬 결과]형식으로 반환. - 이벤트 로그:
SkillActivated(paths: 매칭 시),SkillCompleted(fork 실행 완료 시) JSONL 기록 - DI 패턴:
DelegateAgentTool.SetSubAgentRunner패턴 동일 적용 — AgentLoopService 생성자에서 콜백 주입 - 빌드: 경고 0, 오류 0
최종 업데이트: 2026-04-04 (Phase 2252 + Phase 17-UI-AE + Phase 17-A~D 구현 완료)