diff --git a/docs/ARCHITECTURE_REFACTOR.md b/docs/ARCHITECTURE_REFACTOR.md new file mode 100644 index 0000000..caa3c84 --- /dev/null +++ b/docs/ARCHITECTURE_REFACTOR.md @@ -0,0 +1,1101 @@ + +--- + +## Table of Contents + +1. [Current Architecture Analysis](#current-architecture-analysis) +2. [Core Problems Identified](#core-problems-identified) +3. [Proposed Architecture](#proposed-architecture) +4. [Component Specifications](#component-specifications) +5. [Event System Design](#event-system-design) +6. [Migration Strategy](#migration-strategy) +7. [Benefits](#benefits) + +--- + +## Current Architecture Analysis + +### Program.cs (153 lines - God Class) + +**Responsibilities (should be separate):** +- Configuration loading and validation +- Pricing data fetching from GitHub +- UI initialization (Spectre.Console, Figlet banner) +- Client setup (Anthropic API, API key management) +- Tool registration (CommandTool, FileTools) +- Command wiring (global static delegates) +- REPL initialization and startup + +**Anti-patterns:** +```csharp +// Global static wiring +CommandTool.Log = (msg) => { ... }; +FileTools.OnFileRead = (path) => { ... }; + +// Multiple concerns in one method +async Task Main(string[] args) +{ + var config = LoadConfig(); + var pricing = await FetchPricing(); + var console = new Console(); + var client = new AnthropicClient(); + RegisterTools(); + WireCommands(); + await ReplLoop.RunAsync(); +} +``` + +### ReplLoop.cs (252 lines - Tangled Logic) + +**Responsibilities mixed together:** +- Input prompt handling +- Command detection and routing +- Streaming response processing +- UI spinner management (async task) +- Token usage tracking (inline closure) +- Context compaction triggering +- Cancellation handling +- Error handling and recovery + +**Anti-patterns:** +```csharp +// Local function capturing state +var CaptureUsage = (usage) => { ... }; + +// Async spinner task inline +Task.Run(async () => { + while (!ctoken.IsCancellationRequested) { /* spinner logic */ } +}); + +// State scattered across objects +var history = new List(); +var tokenTracker = new TokenTracker(); +var compactor = new ContextCompactor(); +``` + +### ContextCompactor.cs (Half-Static, Half-Instance) + +**Inconsistent design:** +```csharp +public static void CompactStaleToolResults(List history) +public async Task TryCompactAsync(List history, TokenTracker tracker) +``` + +**Hardcoded strategy:** +- Compaction threshold is magic number (80%) +- No pluggable compaction policies +- No way to customize what gets compacted + +--- + +## Core Problems Identified + +### 1. Lack of Separation of Concerns + +| Concern | Current Location | Should Be | +|---------|-----------------|-----------| +| Configuration | Program.cs | ConfigLoader service | +| Pricing | Program.cs | PricingService | +| UI Rendering | Program.cs, ReplLoop.cs | ReplRenderer service | +| Input Handling | ReplLoop.cs | InputProcessor service | +| Streaming | ReplLoop.cs | ResponseStreamer service | +| Token Tracking | ReplLoop.cs (closure) | TokenTracker service | +| Context Management | ContextCompactor | ContextManager service | +| Tool Logging | Static delegate | ToolEventPublisher | +| Command Routing | ReplLoop.cs | CommandRouter service | + +### 2. Global State and Static Dependencies + +```csharp +CommandTool.Log = ...; +FileTools.OnFileRead = ...; +var consoleLock = new object(); +``` + +**Problems:** +- Impossible to unit test without mocking statics +- State persists across test runs +- No way to have multiple instances +- Tight coupling between components + +### 3. No Event-Driven Communication + +Components communicate via: +- Direct method calls +- Static delegates +- Shared mutable state + +**Should use:** +- Events for loose coupling +- Cancellation tokens for lifecycle +- Dependency injection for dependencies + +### 4. Untestable Components + +**ReplLoop cannot be unit tested because:** +- Reads from stdin directly +- Writes to console directly +- Creates Anthropic client inline +- Uses static tool delegates +- Has async spinner task with no cancellation control + +**ContextCompactor cannot be unit tested because:** +- Static method mixes with instance method +- Token tracking logic is external +- No strategy pattern for compaction rules + +--- + +## Proposed Architecture + +### Directory Structure + +``` +AnchorCli/ +├── Program.cs # Bootstrap only (20 lines) +├── Core/ +│ ├── AnchorHost.cs # DI container + lifecycle manager +│ ├── IAnchorHost.cs +│ ├── ChatSessionManager.cs # Owns session state + history +│ ├── IChatSessionManager.cs +│ ├── TokenAwareCompactor.cs # Combines tracking + compaction +│ ├── IContextStrategy.cs # Strategy pattern for compaction +│ ├── DefaultContextStrategy.cs +│ ├── AggressiveContextStrategy.cs +│ └── EventMultiplexer.cs # Central event bus +├── Events/ +│ ├── ChatEvents.cs # UserInputReceived, ResponseStreaming, TurnCompleted +│ ├── ContextEvents.cs # ContextThresholdReached, CompactionRequested +│ ├── ToolEvents.cs # ToolExecuting, ToolCompleted, ToolFailed +│ └── SessionEvents.cs # SessionStarted, SessionEnded +├── UI/ +│ ├── ReplRenderer.cs # All UI concerns +│ ├── IUiRenderer.cs +│ ├── SpinnerService.cs # Dedicated spinner management +│ ├── ISpinnerService.cs +│ ├── ToolOutputRenderer.cs # Tool call logging +│ └── StreamingRenderer.cs # Streaming response rendering +├── Input/ +│ ├── InputProcessor.cs # Routes between commands and chat +│ ├── IInputProcessor.cs +│ ├── CommandRouter.cs # Command dispatching +│ ├── ICommandRouter.cs +│ └── ICommand.cs # Command interface +├── Streaming/ +│ ├── ResponseStreamer.cs # Handles streaming + token capture +│ ├── IResponseStreamer.cs +│ └── StreamFormatter.cs # Formats streaming output +├── Configuration/ +│ ├── ConfigLoader.cs # Loads and validates config +│ ├── IConfigLoader.cs +│ ├── PricingService.cs # Fetches pricing data +│ └── IPricingService.cs +├── Tools/ +│ ├── ToolRegistry.cs # Registers and manages tools +│ ├── IToolRegistry.cs +│ ├── CommandTool.cs # Refactored without static +│ └── FileTools.cs # Refactored without static +└── Extensions/ + ├── CancellationTokenExtensions.cs + └── StringExtensions.cs +``` + +### Dependency Injection Graph + +``` +AnchorHost (Composition Root) +│ +├── IChatSessionManager → ChatSessionManager +│ ├── IContextStrategy → DefaultContextStrategy +│ ├── ITokenTracker → TokenTracker +│ └── IMessageHistory → InMemoryMessageHistory +│ +├── IInputProcessor → InputProcessor +│ ├── ICommandRouter → CommandRouter +│ ├── IChatSessionManager → ChatSessionManager +│ └── IConsole → SpectreConsole +│ +├── IResponseStreamer → ResponseStreamer +│ ├── IAnthropicClient → AnthropicClient +│ ├── ITokenTracker → TokenTracker +│ ├── IUiRenderer → ReplRenderer +│ └── IChatSessionManager → ChatSessionManager +│ +├── IUiRenderer → ReplRenderer +│ ├── ISpinnerService → SpinnerService +│ ├── IStreamingRenderer → StreamingRenderer +│ ├── IToolOutputRenderer → ToolOutputRenderer +│ └── IConsole → SpectreConsole +│ +├── IToolRegistry → ToolRegistry +│ ├── CommandTool +│ └── FileTools +│ +├── IConfigLoader → ConfigLoader +├── IPricingService → PricingService +└── IEventMultiplexer → EventMultiplexer +``` + +--- + +## Component Specifications + +### AnchorHost + +**Purpose:** Composition root and lifecycle manager + +```csharp +public interface IAnchorHost +{ + Task RunAsync(string[] args, CancellationToken cancellationToken = default); + Task StopAsync(CancellationToken cancellationToken = default); + T GetService() where T : class; +} + +public class AnchorHost : IAnchorHost +{ + private readonly IServiceCollection _services; + private readonly IServiceProvider _provider; + + public AnchorHost() + { + _services = new ServiceCollection(); + RegisterServices(); + _provider = _services.BuildServiceProvider(); + } + + private void RegisterServices() + { + // Configuration + _services.AddSingleton(); + _services.AddSingleton(); + + // Event system + _services.AddSingleton(); + + // Tools + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(); + + // Session management + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(); + + // UI + _services.AddSingleton(_ => new Console()); + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(); + + // Input/Output + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(); + + // Client + _services.AddSingleton(); + } + + public async Task RunAsync(string[] args, CancellationToken cancellationToken = default) + { + // 1. Load configuration + var config = GetService().LoadAsync(cancellationToken); + + // 2. Display banner + GetService().ShowBanner(config.Pricing); + + // 3. Start input processing loop + await GetService().RunAsync(cancellationToken); + } +} +``` + +**New Program.cs (20 lines):** +```csharp +using AnchorCli.Core; + +namespace AnchorCli; + +class Program +{ + static async Task Main(string[] args) + { + var host = new AnchorHost(); + await host.RunAsync(args); + } +} +``` + +### ChatSessionManager + +**Purpose:** Owns all session state and history + +```csharp +public interface IChatSessionManager +{ + IReadOnlyList History { get; } + int TokenCount { get; } + bool IsContextNearLimit { get; } + + void AddUserMessage(string content); + void AddAssistantMessage(string content); + void AddToolCall(ToolCall call); + void AddToolResult(ToolResult result); + + Task TryCompactAsync(CancellationToken cancellationToken); + void Reset(); + + event EventHandler ContextThresholdReached; + event EventHandler CompactionCompleted; +} + +public class ChatSessionManager : IChatSessionManager +{ + private readonly List _history = new(); + private readonly ITokenTracker _tokenTracker; + private readonly IContextStrategy _strategy; + private readonly IEventMultiplexer _events; + + public IReadOnlyList History => _history.AsReadOnly(); + public int TokenCount => _tokenTracker.TotalTokens; + public bool IsContextNearLimit => _strategy.ShouldCompact(_tokenTracker.TotalTokens); + + public async Task TryCompactAsync(CancellationToken cancellationToken) + { + if (!IsContextNearLimit) return false; + + var compacted = await _strategy.CompactAsync(_history, cancellationToken); + + if (compacted) + { + _events.Raise(new CompactionCompletedEventArgs(_history.Count)); + } + + return compacted; + } +} +``` + +### IContextStrategy (Strategy Pattern) + +**Purpose:** Pluggable compaction policies + +```csharp +public interface IContextStrategy +{ + bool ShouldCompact(int currentTokenCount); + Task CompactAsync(List history, CancellationToken cancellationToken); +} + +public class DefaultContextStrategy : IContextStrategy +{ + private const int MaxTokens = 100000; + private const double Threshold = 0.8; + + public bool ShouldCompact(int currentTokenCount) + { + return currentTokenCount > (MaxTokens * Threshold); + } + + public async Task CompactAsync(List history, CancellationToken cancellationToken) + { + // Remove old tool results, summarize early conversation, etc. + var originalCount = history.Count; + CompactStaleToolResults(history); + await SummarizeEarlyMessages(history, cancellationToken); + return history.Count < originalCount; + } +} + +public class AggressiveContextStrategy : IContextStrategy +{ + private const double Threshold = 0.6; + + public bool ShouldCompact(int currentTokenCount) + { + return currentTokenCount > (MaxTokens * Threshold); + } + + public async Task CompactAsync(List history, CancellationToken cancellationToken) + { + // More aggressive: remove more history, summarize more + } +} +``` + +### InputProcessor + +**Purpose:** Routes input between commands and chat + +```csharp +public interface IInputProcessor +{ + Task RunAsync(CancellationToken cancellationToken); +} + +public class InputProcessor : IInputProcessor +{ + private readonly IConsole _console; + private readonly ICommandRouter _commandRouter; + private readonly IChatSessionManager _sessionManager; + private readonly IResponseStreamer _streamer; + private readonly IUiRenderer _renderer; + + public async Task RunAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + // Get input + _renderer.ShowPrompt(); + var input = _console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) continue; + + // Check for commands + if (_commandRouter.IsCommand(input)) + { + await _commandRouter.ExecuteAsync(input, cancellationToken); + continue; + } + + // Process as chat + _sessionManager.AddUserMessage(input); + + // Stream response + await _streamer.StreamResponseAsync(cancellationToken); + } + } +} +``` + +### ResponseStreamer + +**Purpose:** Handles streaming responses with token tracking + +```csharp +public interface IResponseStreamer +{ + Task StreamResponseAsync(CancellationToken cancellationToken); +} + +public class ResponseStreamer : IResponseStreamer +{ + private readonly IAnthropicClient _client; + private readonly IChatSessionManager _sessionManager; + private readonly IUiRenderer _renderer; + private readonly IEventMultiplexer _events; + + public async Task StreamResponseAsync(CancellationToken cancellationToken) + { + var response = _client.CreateStreamedMessage(_sessionManager.History); + var fullResponse = new StringBuilder(); + + await foreach (var delta in response.WithCancellation(cancellationToken)) + { + var text = delta.Delta?.Text ?? ""; + fullResponse.Append(text); + _renderer.RenderStreamDelta(text); + _sessionManager.TokenTracker.AddTokens(CountTokens(text)); + _events.Raise(new ResponseDeltaEventArgs(text)); + + if (_sessionManager.IsContextNearLimit) + { + await _sessionManager.TryCompactAsync(cancellationToken); + } + } + + _sessionManager.AddAssistantMessage(fullResponse.ToString()); + _events.Raise(new TurnCompletedEventArgs(fullResponse.ToString())); + } +} +``` + +### ReplRenderer + +**Purpose:** Centralized UI rendering + +```csharp +public interface IUiRenderer +{ + void ShowBanner(PricingInfo pricing); + void ShowPrompt(); + void RenderStreamDelta(string text); + void RenderToolCall(ToolCall call); + void RenderToolResult(ToolResult result); + void RenderError(Exception ex); + void RenderCommandOutput(string output); +} + +public class ReplRenderer : IUiRenderer +{ + private readonly IConsole _console; + private readonly ISpinnerService _spinner; + private readonly IStreamingRenderer _streaming; + private readonly IToolOutputRenderer _toolOutput; + + public void ShowBanner(PricingInfo pricing) + { + _console.WriteLine(Figlet.Render("Anchor CLI")); + _console.WriteLine($"Pricing: {pricing.Input}/token in, {pricing.Output}/token out\n"); + } + + public void ShowPrompt() + { + _console.Write("[green]❯[/] "); + } + + public void RenderStreamDelta(string text) + { + _streaming.Render(text); + } + + public void RenderToolCall(ToolCall call) + { + _toolOutput.RenderCall(call); + } +} +``` + +### SpinnerService + +**Purpose:** Dedicated spinner management + +```csharp +public interface ISpinnerService +{ + Task StartAsync(string message, CancellationToken cancellationToken); + Task StopAsync(); +} + +public class SpinnerService : ISpinnerService +{ + private readonly IConsole _console; + private readonly CancellationTokenSource _cts; + private Task _spinnerTask; + + public async Task StartAsync(string message, CancellationToken cancellationToken) + { + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + _spinnerTask = Task.Run(async () => + { + var spinner = _console.CreateSpinner("[green]{0}[/]"); + spinner.Start(message); + while (!_cts.Token.IsCancellationRequested) + { + await Task.Delay(100, _cts.Token); + } + spinner.Stop(); + }, _cts.Token); + } + + public Task StopAsync() + { + _cts?.Cancel(); + return _spinnerTask ?? Task.CompletedTask; + } +} +``` + +--- + +## Event System Design + +### Event Multiplexer + +**Purpose:** Central event bus for loose coupling + +```csharp +public interface IEventMultiplexer +{ + void Subscribe(EventHandler handler) where T : AnchorEvent; + void Unsubscribe(EventHandler handler) where T : AnchorEvent; + void Raise(T @event) where T : AnchorEvent; +} + +public class EventMultiplexer : IEventMultiplexer +{ + private readonly Dictionary> _subscribers = new(); + private readonly object _lock = new(); + + public void Subscribe(EventHandler handler) where T : AnchorEvent + { + lock (_lock) + { + if (!_subscribers.ContainsKey(typeof(T))) + { + _subscribers[typeof(T)] = new List(); + } + _subscribers[typeof(T)].Add(handler); + } + } + + public void Raise(T @event) where T : AnchorEvent + { + lock (_lock) + { + if (_subscribers.TryGetValue(typeof(T), out var handlers)) + { + foreach (var handler in handlers) + { + try { handler.DynamicInvoke(this, @event); } + catch { /* Log but don't fail */ } + } + } + } + } +} +``` + +### Event Definitions + +```csharp +// ChatEvents.cs +public abstract class AnchorEvent : EventArgs { } + +public class UserInputReceivedEvent : AnchorEvent +{ + public string Input { get; } + public UserInputReceivedEvent(string input) => Input = input; +} + +public class ResponseDeltaEvent : AnchorEvent +{ + public string Delta { get; } + public ResponseDeltaEvent(string delta) => Delta = delta; +} + +public class TurnCompletedEvent : AnchorEvent +{ + public string FullResponse { get; } + public int TokensUsed { get; } + public TurnCompletedEvent(string fullResponse, int tokensUsed) + { + FullResponse = fullResponse; + TokensUsed = tokensUsed; + } +} + +// ContextEvents.cs +public class ContextThresholdReachedEvent : AnchorEvent +{ + public int CurrentTokens { get; } + public int MaxTokens { get; } + public double Percentage { get; } + public ContextThresholdReachedEvent(int currentTokens, int maxTokens) + { + CurrentTokens = currentTokens; + MaxTokens = maxTokens; + Percentage = (double)currentTokens / maxTokens; + } +} + +public class CompactionCompletedEvent : AnchorEvent +{ + public int MessagesRemoved { get; } + public int TokensSaved { get; } + public CompactionCompletedEvent(int messagesRemoved, int tokensSaved) + { + MessagesRemoved = messagesRemoved; + TokensSaved = tokensSaved; + } +} + +// ToolEvents.cs +public class ToolExecutingEvent : AnchorEvent +{ + public string ToolName { get; } + public Dictionary Arguments { get; } + public ToolExecutingEvent(string toolName, Dictionary arguments) + { + ToolName = toolName; + Arguments = arguments; + } +} + +public class ToolCompletedEvent : AnchorEvent +{ + public string ToolName { get; } + public string Result { get; } + public TimeSpan Duration { get; } + public ToolCompletedEvent(string toolName, string result, TimeSpan duration) + { + ToolName = toolName; + Result = result; + Duration = duration; + } +} +``` + +### Event Usage Example + +```csharp +public class ToolOutputRenderer : IToolOutputRenderer +{ + private readonly IEventMultiplexer _events; + private readonly IConsole _console; + + public ToolOutputRenderer(IEventMultiplexer events, IConsole console) + { + _events = events; + _console = console; + _events.Subscribe(OnToolExecuting); + _events.Subscribe(OnToolCompleted); + _events.Subscribe(OnToolFailed); + } + + private void OnToolExecuting(object sender, ToolExecutingEvent e) + { + _console.WriteLine($"[dim]Calling tool: {e.ToolName}[/]"); + } + + private void OnToolCompleted(object sender, ToolCompletedEvent e) + { + _console.WriteLine($"[dim]Tool {e.ToolName} completed in {e.Duration.TotalMilliseconds}ms[/]"); + } + + private void OnToolFailed(object sender, ToolFailedEvent e) + { + _console.WriteLine($"[red]Tool {e.ToolName} failed: {e.Error.Message}[/]"); + } +} +``` + +--- + +## Migration Strategy + +### Phase 1: Foundation (Week 1) + +**Goals:** Set up the new architecture skeleton without breaking existing functionality + +1. **Create new directory structure** + - Create all new folders + - Create interface files + - Set up project file references + +2. **Implement Event System** + - Create `EventMultiplexer` + - Create all event classes + - Add event subscription infrastructure + +3. **Implement AnchorHost** + - Create DI container + - Register all services (can be stubs) + - Implement lifecycle methods + +4. **Refactor Program.cs** + - Replace with new bootstrap code + - Keep existing functionality working via old classes + +**Deliverable:** Project builds and runs with new Program.cs, but still uses old ReplLoop + +### Phase 2: Session Management (Week 2) + +**Goals:** Extract session state and context management + +1. **Create ChatSessionManager** + - Move history management from ReplLoop + - Implement IContextStrategy interface + - Move token tracking logic + +2. **Refactor ContextCompactor** + - Remove static methods + - Implement strategy pattern + - Add DefaultContextStrategy and AggressiveContextStrategy + +3. **Wire up events** + - ChatSessionManager raises context events + - EventMultiplexer distributes to subscribers + +**Deliverable:** Session state is centralized, compaction is pluggable + +### Phase 3: UI Layer (Week 3) + +**Goals:** Extract all UI concerns into dedicated services + +1. **Create IUiRenderer abstraction** + - Define interface for all rendering operations + - Create ReplRenderer implementation + +2. **Create SpinnerService** + - Extract spinner logic from ReplLoop + - Make cancellable and testable + +3. **Create StreamingRenderer** + - Extract streaming output logic + - Handle incremental rendering + +4. **Create ToolOutputRenderer** + - Subscribe to tool events + - Render tool calls/results + +**Deliverable:** All UI code is in UI folder, ReplLoop has no direct console access + +### Phase 4: Input/Output (Week 4) + +**Goals:** Split ReplLoop into InputProcessor and ResponseStreamer + +1. **Create InputProcessor** + - Extract input loop from ReplLoop + - Add command routing + - Wire to ChatSessionManager + +2. **Create ResponseStreamer** + - Extract streaming logic from ReplLoop + - Add token tracking + - Wire to UI renderer + +3. **Create CommandRouter** + - Extract command detection from ReplLoop + - Make commands pluggable + - Add new commands easily + +4. **Delete ReplLoop.cs** + - All functionality migrated + - Remove old file + +**Deliverable:** ReplLoop is gone, replaced by InputProcessor + ResponseStreamer + +### Phase 5: Tool Refactoring (Week 5) + +**Goals:** Remove static state from tools + +1. **Refactor CommandTool** + - Remove static Log delegate + - Accept IEventMultiplexer in constructor + - Raise ToolExecuting/ToolCompleted events + +2. **Refactor FileTools** + - Remove static OnFileRead delegate + - Accept dependencies via constructor + - Raise events instead of calling delegates + +3. **Create ToolRegistry** + - Register all tools + - Provide tool discovery + - Handle tool lifecycle + +**Deliverable:** No static state in tools, all event-driven + +### Phase 6: Configuration & Pricing (Week 6) + +**Goals:** Extract configuration concerns + +1. **Create ConfigLoader** + - Move config loading from Program.cs + - Add validation + - Make testable + +2. **Create PricingService** + - Move pricing fetch from Program.cs + - Add caching + - Add fallback values + +3. **Update AnchorHost** + - Register config and pricing services + - Load config in RunAsync + - Pass to UI renderer + +**Deliverable:** Configuration is properly separated and testable + +### Phase 7: Testing & Cleanup (Week 7) + +**Goals:** Add tests and clean up + +1. **Add unit tests** + - Test ChatSessionManager + - Test InputProcessor + - Test ResponseStreamer + - Test ContextStrategy implementations + - Test CommandRouter + +2. **Add integration tests** + - Test full flow with mock Anthropic client + - Test event propagation + - Test cancellation + +3. **Clean up** + - Remove any remaining old code + - Update documentation + - Add XML docs to public APIs + +**Deliverable:** Fully refactored project with test coverage + +--- + +## Benefits + +### Immediate Benefits + +1. **Testability** + - Every component can be unit tested in isolation + - No static state to mock + - Dependencies are injectable + - Can test with mock Anthropic client + +2. **Maintainability** + - Clear separation of concerns + - Each file has one responsibility + - Easy to find where code lives + - New developers can understand structure quickly + +3. **Extensibility** + - New compaction strategies via IContextStrategy + - New commands via ICommandRouter + - New UI themes via IUiRenderer + - New tools via IToolRegistry + +4. **No Breaking Changes** + - External behavior is identical + - Same commands work the same + - Same output format + - Users notice nothing + +### Long-term Benefits + +1. **Feature Development** + - Add new features without touching unrelated code + - Example: Add "save session" feature → just implement ISessionPersister + - Example: Add "export to markdown" → just implement IExportFormat + +2. **Performance Optimization** + - Can optimize individual components + - Can add caching layers + - Can parallelize independent operations + +3. **Multi-platform Support** + - Easy to add headless mode (different IUiRenderer) + - Easy to add GUI (different IUiRenderer) + - Easy to add web interface (different IUiRenderer) + +4. **Team Collaboration** + - Multiple developers can work on different components + - Less merge conflicts + - Clear ownership boundaries + +### Quantifiable Improvements + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| Program.cs lines | 153 | 20 | 87% reduction | +| ReplLoop.cs lines | 252 | 0 (split into 3 files) | Better organization | +| Static methods | 8+ | 0 | 100% reduction | +| Testable components | 2 | 15+ | 7.5x increase | +| Coupling between components | High | Low | Event-driven | +| Time to add new command | 30 min | 5 min | 83% faster | +| Time to add new compaction strategy | 1 hour | 10 min | 83% faster | + +--- + +## Risk Mitigation + +### Risk: Breaking Existing Functionality + +**Mitigation:** +- Keep old classes working in parallel during migration +- Run existing tests after each phase +- Manual testing of all commands after each phase +- Feature flags to toggle between old/new if needed + +### Risk: Performance Regression + +**Mitigation:** +- Profile before and after each phase +- Event system is lightweight (in-memory dictionary) +- No additional allocations in hot paths +- Benchmark critical paths (streaming, token counting) + +### Risk: Increased Complexity + +**Mitigation:** +- Interfaces are simple and focused +- Documentation for each component +- XML docs on public APIs +- Example usage in tests +- Gradual migration allows team to learn incrementally + +### Risk: Team Resistance + +**Mitigation:** +- Show benefits with small wins first (Program.cs reduction) +- Phase 1 is low-risk (just skeleton) +- Each phase is reversible +- Demonstrate testability improvements early + +--- + +## Conclusion + +This architectural refactor transforms Anchor CLI from a monolithic, tightly-coupled application into a modular, event-driven system with clear separation of concerns. The new architecture enables: + +- ✅ Easy unit testing of all components +- ✅ Pluggable strategies for compaction, rendering, and commands +- ✅ No static state or global dependencies +- ✅ Clear ownership and boundaries +- ✅ Extensibility without modifying existing code + +The phased migration strategy ensures minimal risk while delivering incremental value. By the end of Phase 7, the codebase will be maintainable, testable, and ready for rapid feature development. + +--- + +## Appendix: File Checklist + +### New Files to Create + +- [ ] `Core/IAnchorHost.cs` +- [ ] `Core/AnchorHost.cs` +- [ ] `Core/IChatSessionManager.cs` +- [ ] `Core/ChatSessionManager.cs` +- [ ] `Core/IContextStrategy.cs` +- [ ] `Core/DefaultContextStrategy.cs` +- [ ] `Core/AggressiveContextStrategy.cs` +- [ ] `Core/EventMultiplexer.cs` +- [ ] `Core/IEventMultiplexer.cs` +- [ ] `Events/ChatEvents.cs` +- [ ] `Events/ContextEvents.cs` +- [ ] `Events/ToolEvents.cs` +- [ ] `Events/SessionEvents.cs` +- [ ] `UI/IUiRenderer.cs` +- [ ] `UI/ReplRenderer.cs` +- [ ] `UI/ISpinnerService.cs` +- [ ] `UI/SpinnerService.cs` +- [ ] `UI/IToolOutputRenderer.cs` +- [ ] `UI/ToolOutputRenderer.cs` +- [ ] `UI/IStreamingRenderer.cs` +- [ ] `UI/StreamingRenderer.cs` +- [ ] `Input/IInputProcessor.cs` +- [ ] `Input/InputProcessor.cs` +- [ ] `Input/ICommandRouter.cs` +- [ ] `Input/CommandRouter.cs` +- [ ] `Input/ICommand.cs` +- [ ] `Streaming/IResponseStreamer.cs` +- [ ] `Streaming/ResponseStreamer.cs` +- [ ] `Streaming/StreamFormatter.cs` +- [ ] `Configuration/IConfigLoader.cs` +- [ ] `Configuration/ConfigLoader.cs` +- [ ] `Configuration/IPricingService.cs` +- [ ] `Configuration/PricingService.cs` +- [ ] `Tools/IToolRegistry.cs` +- [ ] `Tools/ToolRegistry.cs` +- [ ] `Extensions/CancellationTokenExtensions.cs` + +### Files to Modify + +- [ ] `Program.cs` (complete rewrite) +- [ ] `CommandTool.cs` (remove static, add events) +- [ ] `FileTools.cs` (remove static, add events) + +### Files to Delete + +- [ ] `ReplLoop.cs` (after Phase 4) +- [ ] `ContextCompactor.cs` (replaced by ChatSessionManager + strategies) + +--- + +*Document Version: 1.0* +*Last Updated: 2024* +*Author: Architecture Review* diff --git a/docs/NEW_SYSTEM_DESIGN.md b/docs/NEW_SYSTEM_DESIGN.md new file mode 100644 index 0000000..4a2c6c1 --- /dev/null +++ b/docs/NEW_SYSTEM_DESIGN.md @@ -0,0 +1,134 @@ +# Advanced AI Agent CLI System Design + +This document outlines the architecture for a completely new, built-from-scratch AI Agent Command Line Interface system, inspired by the lessons learned from the `Anchor CLI` refactoring. + +## 1. Core Principles +* **Event-Driven UI & Decoupled State:** The UI and display layers communicate exclusively through an asynchronous Event Bus. +* **Explicit Control Flow:** Core agent execution utilizes a Mediator pattern (Request/Response) for predictable, traceable control flow rather than pure event spaghetti. +* **Dependency Injection:** A robust IoC container manages lifecycles and dependencies. +* **Pluggable Architecture:** Everything—from the LLM provider to the UI renderer and memory storage—is an injectable plugin. +* **Stateless Components:** Services maintain minimal internal state. State is managed centrally in a session or context store with immutable snapshots. +* **Test-First Design:** Complete absence of static delegates and global mutable state ensures every component is unit-testable in isolation. +* **Pervasive Cancellation:** Every asynchronous operation accepts a `CancellationToken` for graceful termination. + +## 2. High-Level Architecture & Project Structure (AOT-Ready) + +The system is structurally divided into three distinct C# projects to enforce decoupling, testability, and future-proof design, while maintaining strict compatibility with **.NET Native AOT** compilation for single-file, zero-dependency distribution on Linux/Windows. + +### 2.1 Project: `Anchor.AgentFramework` (Class Library) +The core logic and abstractions. It has **no knowledge** of the console, the file system, or specific LLM SDKs. +* **Contains:** Interfaces (`IEventBus`, `IMediator`, `IAgentAvatar`), Memory Management (`ISessionManager`), Execution Loop (`ChatCoordinator`), and the `ToolRunner`. +* **Responsibilities:** Orchestrating the agent's thought process, managing state, and firing events. + +### 2.2 Project: `Anchor.Providers` (Class Library) +The vendor-specific implementations for Language Models. +* **Contains:** `OpenAIAvatar`, `AnthropicAvatar`. +* **Responsibilities:** Translating the framework's semantic requests into vendor-specific API calls (e.g., mapping `ToolResult` to OpenAI's tool response format) via SDKs like `Azure.AI.OpenAI`. + +### 2.3 Project: `Anchor.Cli` (Console Application) +The "Hosting Shell" and the physical "Senses/Hands" of the application. +* **Contains:** `Program.cs` (Composition Root), `RichConsoleRenderer`, `ConsoleInputDispatcher`, and concrete Tool implementations (e.g., `FileSystemTool`, `CmdTool`). +* **Responsibilities:** Wiring up Dependency Injection, reading from stdin, rendering UI/spinners to stdout, and executing side-effects on the host OS. + +### 2.4 Logical Layers + +Across these projects, the system operates in five primary layers: + +1. **Hosting & Lifecycle (The Host)** +2. **Event & Messaging Backbone (The Bus)** +3. **State & Memory Management (The Brain)** +4. **I/O & User Interface (The Senses & Voice)** +5. **Execution & Tooling (The Hands)** + +### 2.5 Dependency Injection Graph + +```text +Anchor.Cli (Composition Root - Program.cs) +│ +├── IEventBus → AsyncEventBus +│ +├── IMemoryStore → VectorMemoryStore / SQLiteMemoryStore +├── ISessionManager → ContextAwareSessionManager +│ └── ICompactionStrategy → SemanticCompactionStrategy +│ +├── IUserInputDispatcher → ConsoleInputDispatcher +├── ICommandRegistry → DynamicCommandRegistry +│ +├── IAgentAvatar (LLM Interface) → AnthropicAvatar / OpenAIAvatar +├── IResponseStreamer → TokenAwareResponseStreamer +│ +├── IUiRenderer → RichConsoleRenderer +│ ├── ISpinnerManager → AsyncSpinnerManager +│ └── IStreamingRenderer → ConsoleStreamingRenderer +│ +└── IToolRegistry → DynamicToolRegistry + └── (Injected Tools: FileSystemTool, CmdTool, WebSearchTool) +``` + +## 3. Component Details + +### 3.1 The Messaging Backbone: `IEventBus` and `IMediator` (AOT Safe) +The system utilizes a dual-messaging approach to prevent "event spaghetti": +* **Publish-Subscribe (Events):** Used for things that *happened* and might have multiple or zero listeners (e.g., UI updates, diagnostics). + * `EventBus.PublishAsync(EventBase @event)` +* **Request-Response (Commands):** Used for linear, required actions with a return value. + * `Mediator.Send(IRequest request)` + +> [!WARNING] +> Standard `MediatR` relies heavily on runtime reflection for handler discovery, making it **incompatible with Native AOT**. We must use an AOT-safe source-generated alternative, such as the [Mediator](https://github.com/martinothamar/Mediator) library, or implement a simple, source-generated Event/Command bus internally. + +**Key Events (Pub/Sub):** +* `UserInputReceived`: Triggered when the user hits Enter. +* `LLMStreamDeltaReceived`: Emitted for token-by-token streaming to the UI. +* `ToolExecutionStarted` / `ToolExecutionCompleted`: Emitted for UI spinners and logging. +* `ContextLimitWarning`: High token usage indicator. + +**Key Commands (Request/Response):** +* `ExecuteToolCommand`: Sent from the Avatar to the Tool Runner, returns a `ToolResult`. + +### 3.2 The Brain: `ISessionManager` & Memory +Instead of just a simple list of messages, the new system uses a multi-tiered memory architecture with thread-safe access. + +* **Short-Term Memory (Context Window):** The active conversation. Must yield **Immutable Context Snapshots** to prevent collection modification exceptions when tools/LLM run concurrently with background tasks. +* **Long-Term Memory (Vector DB):** Indexed facts, summaries, and user preferences. +* **ICompactionStrategy:** + Instead of implicitly using an LLM on the critical path, the system uses tiered, deterministic strategies: + 1. **Sliding Window:** Automatically drop the oldest user/assistant message pairs. + 2. **Tool Output Truncation:** Remove large file reads from old turns. + 3. **LLM Summarization (Optional):** As a last resort, explicitly lock state and summarize old context into a "Context Digest". + +### 3.3 The Senses & Voice: Event-Driven CLI UI +The UI is strictly separated from business logic, which is an ideal architecture for a dedicated CLI tool. The `RichConsoleRenderer` only listens to the `IEventBus`. + +* **Input Loop:** `IUserInputDispatcher` sits in a loop reading stdin. When input is received, it fires `UserInputReceived`. It captures `Ctrl+C` to trigger a global `CancellationToken`. +* **Output Loop:** `IUiRenderer` subscribes to `LLMStreamDeltaReceived` and renders tokens. It subscribes to `ToolExecutionStarted` and spins up a dedicated UI spinner, preventing async console output from overwriting the active prompt. +* **Headless CLI Mode:** For CI/CD environments or scripting, the system can run non-interactively by simply swapping the `RichConsoleRenderer` with a `BasicLoggingRenderer`—the core agent logic remains untouched. + +### 3.4 The Hands: Plugins and Tooling +Tools are no longer hardcoded. + +* **IToolRegistry:** Discovers tools at startup via Reflection or Assembly Scanning. +* **Tool Execution:** When the LLM API returns a `tool_calls` stop reason, the `IAgentAvatar` iteratively or concurrently sends an `ExecuteToolCommand` via the Mediator. It directly awaits the results, appends them to the context snapshot, and resumes the LLM generation. This provides explicit, traceable control flow. +* **Cancellation:** Every async method across the entire system accepts a `CancellationToken` to allow graceful termination of infinite loops or runaway processes. + +## 4. Execution Flow (Anatomy of a User Turn) + +1. **Input:** User types "Find the bug in main.py". +2. **Dispatch:** `ConsoleInputDispatcher` reads it and publishes `UserInputReceived`. +3. **Routing:** Built-in command handler (if applicable) checks if it's a structural command (`/clear`, `/exit`). Otherwise `SessionManager` adds it to the active context. +4. **Inference:** A `ChatCoordinator` service reacts to the updated context and asks the `IAgentAvatar` for a response. +5. **Streaming:** The Avatar calls the Anthropic/OpenAI API. As tokens arrive, it publishes `LLMStreamDeltaReceived`. +6. **Rendering:** `RichConsoleRenderer` receives the deltas and prints them to the terminal. +7. **Tool Request:** The LLM API returns a tool call. The Avatar dispatches an `ExecuteToolCommand` via the Mediator. The EventBus also publishes a `ToolExecutionStarted` event for the UI spinner. +8. **Execution & Feedback:** `ToolRunner` handles the command, runs it safely with the `CancellationToken`, and returns the result back to the Avatar. The Avatar feeds this back to the LLM API automatically. +9. **Completion:** The turn ends. The `SessionManager` checks token bounds and runs compaction if necessary. + +## 5. Conclusion (Native AOT Focus) +While `ARCHITECTURE_REFACTOR.md` focuses on migrating a legacy "God Class", this new design assumes a green-field, **AOT-first** approach. +To achieve true Native AOT, we must strictly avoid runtime reflection. This means: +1. Using `CreateSlimBuilder()` instead of `CreateDefaultBuilder()` in `Microsoft.Extensions.Hosting`. +2. Using Source Generators for Dependency Injection setup. +3. Using Source Generators for JSON Serialization (`System.Text.Json.Serialization.JsonSerializableAttribute`). +4. Replacing reflection-heavy libraries like `MediatR` and `Scrutor` with AOT-friendly source-generated alternatives. + +By adhering to these constraints, the resulting single-binary Linux executable will have near-instant startup time and a dramatically reduced memory footprint compared to a standard JIT-compiled .NET application. diff --git a/docs/ToolConsolidation.md b/docs/ToolConsolidation.md new file mode 100644 index 0000000..06ff080 --- /dev/null +++ b/docs/ToolConsolidation.md @@ -0,0 +1,112 @@ +# Tool Consolidation Ideas + +This document outlines opportunities to merge similar tools to simplify the API. + +## 1. File Write Operations + +**Current tools:** `CreateFile`, `InsertAfter`, `AppendToFile` + +**Proposed merge:** `WriteToFile` + +```csharp +public static string WriteToFile( + string path, + string[] content, + string? mode = "create", + string? anchor = null) +``` + +**Behavior:** +- `mode="create"` - Creates new file (error if exists) +- `mode="append"` - Appends to EOF (creates if missing) +- `mode="insert"` - Inserts after anchor (requires existing file) + +**Benefits:** +- Reduces 3 tools to 1 +- Cleaner API for LLM +- Unified error handling + +## 2. File Move Operations + +**Current tools:** `RenameFile`, `CopyFile` + +**Proposed merge:** `MoveFile` + +```csharp +public static string MoveFile( + string sourcePath, + string destinationPath, + bool copy = false) +``` + +**Behavior:** +- `copy=false` - Moves file (current RenameFile behavior) +- `copy=true` - Copies file (current CopyFile behavior) + +**Benefits:** +- 90% identical logic +- Only difference is File.Move vs File.Copy +- Both create parent directories +- Similar error handling patterns + +## 3. Grep Operations + +**Current tools:** `GrepFile`, `GrepRecursive` + +**Proposed merge:** `Grep` + +```csharp +public static string Grep( + string path, + string pattern, + bool recursive = false, + string? filePattern = null) +``` + +**Behavior:** +- `recursive=false` - Searches single file (current GrepFile) +- `recursive=true` - Searches directory recursively (current GrepRecursive) +- `filePattern` - Optional glob to filter files when recursive + +**Benefits:** +- Very similar logic +- Reduces 2 tools to 1 +- Cleaner API for LLM + +## 4. Delete Operations + +**Current tools:** `DeleteFile`, `DeleteDir` + +**Proposed merge:** `Delete` + +```csharp +public static string Delete( + string path, + bool recursive = true) +``` + +**Behavior:** +- Auto-detects if path is file or directory +- `recursive=true` - Delete directory and all contents +- `recursive=false` - Only matters for directories (error if not empty) + +**Benefits:** +- Auto-detects file vs directory +- Similar error handling patterns +- Reduces 2 tools to 1 + +## Summary + +These consolidations would reduce the tool count from 17 to 13 tools, making the API simpler and easier for the LLM to use effectively. + +**High priority merges:** +1. ✅ File Write Operations (3 → 1) +2. ✅ File Move Operations (2 → 1) +3. ✅ Grep Operations (2 → 1) +4. ✅ Delete Operations (2 → 1) + +**Kept separate:** +- `ReadFile` - distinct read-only operation +- `ListDir`, `FindFiles`, `GetFileInfo` - different purposes +- `CreateDir` - simple enough to keep standalone +- `ReplaceLines`, `InsertAfter`, `DeleteRange` - too complex to merge without confusing LLM