SettingsWindow.AgentConfig (1,202줄): - AgentConfig.cs → 608줄 (등록모델·스킬·프롬프트·AI토글·사내외모드) - AgentHooks.cs → 605줄 (에이전트훅·MCP서버·감사로그·폴백설정) ChatWindow.Presets (1,280줄): - Presets.cs → 315줄 (대화 주제 버튼) - CustomPresets.cs → 978줄 (커스텀 프리셋 관리·하단바·포맷메뉴) ChatWindow.PreviewAndFiles (1,105줄): - PreviewAndFiles.cs → 709줄 (미리보기 패널) - FileBrowser.cs → 408줄 (에이전트 진행률 바·파일 탐색기) WorkflowAnalyzerWindow (929줄): - WorkflowAnalyzerWindow.xaml.cs → 274줄 (리사이즈·탭·데이터수집) - WorkflowAnalyzerWindow.Charts.cs → 667줄 (차트·타임라인·패널·유틸) 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4680 lines
176 KiB
Markdown
4680 lines
176 KiB
Markdown
# 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 레이아웃 구조
|
|
|
|
```xml
|
|
<!-- 전체 레이아웃: 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>
|
|
```
|
|
|
|
**애니메이션 스펙:**
|
|
- 사이드바 확장/축소: `DoubleAnimation` Duration=`0:0:0.2` EasingFunction=`CubicEase EaseInOut`
|
|
- Width: 240 ↔ 48 (GridLength 직접 애니메이션, `GridLengthAnimation` 헬퍼 클래스 사용)
|
|
- 설정 패널 슬라이드: `DoubleAnimation` Duration=`0:0:0.2` EasingFunction=`CubicEase EaseOut`
|
|
- Width: 0 ↔ 300
|
|
- 아이콘 회전 (사이드바 토글 화살표): `RotateTransform` 0 ↔ 180도
|
|
|
|
### 17-UI-3: AgentWindowViewModel
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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):**
|
|
```xml
|
|
<!-- 탭 선택 -->
|
|
<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
|
|
|
|
```csharp
|
|
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 설정
|
|
- 암호화/보안 설정
|
|
```
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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) 핵심:**
|
|
```xml
|
|
<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: 핵심 신규 클래스
|
|
|
|
```csharp
|
|
// 반성 메모리 엔트리
|
|
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 통합 포인트
|
|
|
|
```csharp
|
|
// 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)
|
|
|
|
```csharp
|
|
// 기존 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: 설정 스키마 변경
|
|
|
|
```csharp
|
|
// 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)
|
|
|
|
```csharp
|
|
// 세션 전체에 걸쳐 유지되는 구조화된 작업 상태
|
|
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 이벤트 로그
|
|
|
|
```csharp
|
|
// 모든 에이전트 이벤트의 구조화된 기록
|
|
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: 설정 스키마 변경
|
|
|
|
```csharp
|
|
// 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: 훅 이벤트 열거형 확장
|
|
|
|
```csharp
|
|
// 기존 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 패턴)
|
|
|
|
```csharp
|
|
// 훅 실행기 인터페이스 (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: 훅 속성 모델 확장
|
|
|
|
```csharp
|
|
// 기존 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 확장
|
|
|
|
```csharp
|
|
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 통합
|
|
|
|
```csharp
|
|
// 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: 설정 스키마 변경
|
|
|
|
```csharp
|
|
// 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에 잔류)
|
|
|
|
```csharp
|
|
// 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 확장
|
|
|
|
```csharp
|
|
// 기존 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
|
|
|
|
```csharp
|
|
// 파일 경로에 따라 적용할 스킬을 자동 선택
|
|
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
|
|
|
|
```csharp
|
|
// 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 확장
|
|
|
|
```csharp
|
|
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 통합
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// .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 명령
|
|
|
|
```csharp
|
|
// 컨텍스트 압축 서비스
|
|
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 슬래시 명령 등록
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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 열거형 확장
|
|
|
|
```csharp
|
|
// 기존 AgentDecisionLevel → PermissionMode 로 개념 통합
|
|
public enum PermissionMode
|
|
{
|
|
Default, // 기존: 잠재적으로 위험한 작업에 확인 요청
|
|
AcceptEdits, // 신규: 파일 편집 자동승인, bash/process 확인 유지
|
|
Plan, // 기존: 읽기 전용, 쓰기 차단
|
|
BypassPermissions // 기존: 모든 확인 건너뜀 (자동화 전용)
|
|
}
|
|
```
|
|
|
|
### 17-F-2: PermissionRule + Chain (Chain of Responsibility 패턴)
|
|
|
|
```csharp
|
|
// 권한 규칙 하나
|
|
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 도구 권한
|
|
|
|
```csharp
|
|
// 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: 설정 스키마 변경
|
|
|
|
```csharp
|
|
// 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__외부서버 ] [패턴: (전체) ] [차단 ▾] [삭제]
|
|
[+ 규칙 추가]
|
|
```
|
|
|
|
```csharp
|
|
// 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 전송 추가
|
|
|
|
```csharp
|
|
// 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 뷰
|
|
|
|
```csharp
|
|
// 기존 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 핵심:**
|
|
```xml
|
|
<!-- 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: 자동 컨텍스트 수집
|
|
|
|
```csharp
|
|
// 사용자 메시지에서 파일명 감지 → 자동 읽기
|
|
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: 코디네이터 에이전트
|
|
|
|
```csharp
|
|
// 코디네이터 모드: 계획/라우팅 전담
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 비동기 서브에이전트 실행 + 완료 알림
|
|
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
|
|
|
|
```csharp
|
|
// 에이전트 타입별 영속 메모리
|
|
// 저장: %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
|
|
|
|
```csharp
|
|
// 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의 이벤트 로그를 활용한 세션 재생 및 분기 재실행
|
|
|
|
```csharp
|
|
// 세션 리플레이 서비스
|
|
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: 플러그인 갤러리
|
|
|
|
```csharp
|
|
// 플러그인 매니페스트
|
|
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) | 최상위 |
|
|
|
|
### 핵심 클래스
|
|
|
|
```csharp
|
|
// 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 통합
|
|
|
|
```csharp
|
|
// 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);
|
|
}
|
|
```
|
|
|
|
### 슬래시 명령 추가
|
|
|
|
```csharp
|
|
// /memory 슬래시 명령 → ChatWindow에서 메모리 파일 인라인 에디터 열기
|
|
// 현재 로드된 모든 메모리 레이어 표시 + 편집 버튼
|
|
public class MemorySlashCommand : ISlashCommand
|
|
{
|
|
public string Name => "memory";
|
|
public string Description => "로드된 메모리 파일 목록 및 편집";
|
|
public Task ExecuteAsync(string args, IChatContext ctx);
|
|
}
|
|
```
|
|
|
|
### 설정 항목
|
|
|
|
```csharp
|
|
// 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 → 명령 패턴 매칭 + `&&`/`||`/`;`/`|` 복합 명령 서브 커맨드별 검증.
|
|
|
|
### 핵심 클래스
|
|
|
|
```csharp
|
|
// 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 확장
|
|
|
|
```csharp
|
|
// 기존 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; } = ""; // 사용자 표시 이유
|
|
}
|
|
```
|
|
|
|
### 도구 가시성 필터링
|
|
|
|
```csharp
|
|
// 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가지 훅 타입 완성.
|
|
|
|
### 추가 훅 이벤트
|
|
|
|
```csharp
|
|
// HookTypes.cs 확장
|
|
public enum AgentHookEvent
|
|
{
|
|
// 기존
|
|
PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd,
|
|
UserPromptSubmit, PreCompact, PostCompact,
|
|
|
|
// 신규 추가
|
|
PostToolUseFailure, // 도구 실행 실패 시
|
|
SubAgentStart, // 서브에이전트 시작
|
|
SubAgentStop, // 서브에이전트 완료
|
|
PermissionRequest, // 승인 다이얼로그 직전
|
|
PermissionDenied, // 거부 후
|
|
Notification, // UI 알림 발생 시
|
|
CwdChanged, // 작업 폴더 변경 시
|
|
FileChanged, // 감시 파일 변경 시
|
|
ConfigChange, // 설정 파일 변경 시
|
|
}
|
|
```
|
|
|
|
### 훅 타입 완성 (4종)
|
|
|
|
```csharp
|
|
// 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 입력 수정 기능
|
|
|
|
```csharp
|
|
// 훅이 도구 입력을 수정할 수 있도록 허용
|
|
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`` ` 인라인 실행, 유저급 스킬, 네임스페이스, 스킬별 모델 오버라이드 완성.
|
|
|
|
### 스킬 인라인 명령 실행
|
|
|
|
```csharp
|
|
// 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;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 스킬 프론트매터 확장
|
|
|
|
```yaml
|
|
---
|
|
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>)
|
|
```
|
|
|
|
### 스킬 네임스페이스
|
|
|
|
```csharp
|
|
// /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
|
|
|
|
```csharp
|
|
// 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 # 로컬 범위
|
|
```
|
|
|
|
```csharp
|
|
// 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자 절단 → 도구별 출력 예산 + 대형 출력 임시파일 스필오버. 컨텍스트 조립 메모이제이션.
|
|
|
|
### 출력 예산 관리
|
|
|
|
```csharp
|
|
// 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...";
|
|
}
|
|
}
|
|
```
|
|
|
|
### 컨텍스트 조립 메모이제이션
|
|
|
|
```csharp
|
|
// 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 자동 생성 + 스킬/훅 초기 설정 제안.
|
|
|
|
### 핵심 클래스
|
|
|
|
```csharp
|
|
// 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` |
|
|
|
|
```csharp
|
|
// 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 인터페이스 + 레지스트리
|
|
|
|
```csharp
|
|
// 슬래시 명령 인터페이스 (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 — 컨텍스트 압축
|
|
|
|
```csharp
|
|
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 — 대화 초기화
|
|
|
|
```csharp
|
|
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 — 메모리 파일 편집
|
|
|
|
```csharp
|
|
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 — 세션 모델 전환
|
|
|
|
```csharp
|
|
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 — 플랜 모드 토글
|
|
|
|
```csharp
|
|
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 커밋 메시지
|
|
|
|
```csharp
|
|
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 서버 관리
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// /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: 입력 라우팅 통합
|
|
|
|
```csharp
|
|
// 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`)
|
|
|
|
```csharp
|
|
// 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: 스킬 네임스페이싱 (서브디렉토리 콜론 구분)
|
|
|
|
```csharp
|
|
// 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 구문)
|
|
|
|
```csharp
|
|
// 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 통합
|
|
|
|
```csharp
|
|
// 스킬 실행 파이프라인: 인수 치환 → 인라인 셸 → 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
|
|
|
|
```csharp
|
|
// 복합 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
|
|
|
|
```csharp
|
|
// 훅 실행 시 셸 환경변수를 자동 주입
|
|
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
|
|
|
|
```csharp
|
|
// 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 자동 트리거
|
|
|
|
```csharp
|
|
// 컨텍스트 크기 모니터링 → 자동 컴팩션
|
|
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() 자가 비활성화
|
|
|
|
```csharp
|
|
// 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, SessionId
|
|
- `HookEventKind` 확장: `PostToolUseFailure`, `Stop` 추가 (총 17개, CC 동등)
|
|
|
|
### 25-C: 도구 결과 크기 제한 (ToolResultSizer)
|
|
- `ToolResultSizer.Apply()` — 50,000자 초과 시 임시 파일 저장 + head/tail 프리뷰 반환
|
|
- `ToolResultSizeInfo` — Output, WasTruncated, SpilloverFilePath, OriginalLength
|
|
- `CleanupOldResults()` — 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`, `@/absolute` 5레벨 깊이 재귀 해석
|
|
- 순환 참조 감지, 파일 미존재 시 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/UpdatedAt
|
|
- `AgentSessionSummary` — 목록 표시용 경량 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 통합
|
|
- `AgentLoopService` line 339: `ToolEnvironmentContext` 빌드 후 `GetActiveTools(disabled, env)` 오버로드 사용
|
|
- `.git` 폴더 존재 여부로 `HasGitRepo` 자동 감지
|
|
- `GitTool` → `IConditionalTool` 구현 (HasGitRepo=false 시 도구 목록에서 자동 제외)
|
|
|
|
### 29-C: SessionManager → AgentLoopService 통합
|
|
- `RunAsync` finally 블록에서 세션 자동 저장 (`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, DeleteMemory
|
|
- `AgentTypeMemoryRepository` → `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, Terminate
|
|
- `SdkControlRequest` — Host → CLI 요청 (Type + Id + Payload)
|
|
- `SdkMessageType` — 11종: InitializeResult, AssistantToken, AssistantMessage, ToolUseStart, ToolProgress, ToolResult, SessionResult, HookEvent, CanUseTool, Notification, Error
|
|
- `SdkMessage` — 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, permissionMode
|
|
- `AgentTypeRegistry` — 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` 클래스: 루프 상태 공유 (카운터, 통계, 플랜 메타데이터)
|
|
- `ToolCallAction` enum: 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+DropShadow` 30줄 → `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
|
|
|
|
---
|
|
|
|
최종 업데이트: 2026-04-03 (Phase 22~43 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 11차)
|
|
|