1
0
Files
AnchorCli/docs/ARCHITECTURE_REFACTOR.md

31 KiB
Raw Blame History


Table of Contents

  1. Current Architecture Analysis
  2. Core Problems Identified
  3. Proposed Architecture
  4. Component Specifications
  5. Event System Design
  6. Migration Strategy
  7. 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:

// 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:

// 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<Message>();
var tokenTracker = new TokenTracker();
var compactor = new ContextCompactor();

ContextCompactor.cs (Half-Static, Half-Instance)

Inconsistent design:

public static void CompactStaleToolResults(List<Message> history)
public async Task<bool> TryCompactAsync(List<Message> 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

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

public interface IAnchorHost
{
    Task RunAsync(string[] args, CancellationToken cancellationToken = default);
    Task StopAsync(CancellationToken cancellationToken = default);
    T GetService<T>() 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<IConfigLoader, ConfigLoader>();
        _services.AddSingleton<IPricingService, PricingService>();
        
        // Event system
        _services.AddSingleton<IEventMultiplexer, EventMultiplexer>();
        
        // Tools
        _services.AddSingleton<IToolRegistry, ToolRegistry>();
        _services.AddSingleton<CommandTool>();
        _services.AddSingleton<FileTools>();
        
        // Session management
        _services.AddSingleton<IContextStrategy, DefaultContextStrategy>();
        _services.AddSingleton<ITokenTracker, TokenTracker>();
        _services.AddSingleton<IChatSessionManager, ChatSessionManager>();
        
        // UI
        _services.AddSingleton<IConsole>(_ => new Console());
        _services.AddSingleton<ISpinnerService, SpinnerService>();
        _services.AddSingleton<IStreamingRenderer, StreamingRenderer>();
        _services.AddSingleton<IToolOutputRenderer, ToolOutputRenderer>();
        _services.AddSingleton<IUiRenderer, ReplRenderer>();
        
        // Input/Output
        _services.AddSingleton<ICommandRouter, CommandRouter>();
        _services.AddSingleton<IInputProcessor, InputProcessor>();
        _services.AddSingleton<IResponseStreamer, ResponseStreamer>();
        
        // Client
        _services.AddSingleton<IAnthropicClient, AnthropicClient>();
    }
    
    public async Task RunAsync(string[] args, CancellationToken cancellationToken = default)
    {
        // 1. Load configuration
        var config = GetService<IConfigLoader>().LoadAsync(cancellationToken);
        
        // 2. Display banner
        GetService<IUiRenderer>().ShowBanner(config.Pricing);
        
        // 3. Start input processing loop
        await GetService<IInputProcessor>().RunAsync(cancellationToken);
    }
}

New Program.cs (20 lines):

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

public interface IChatSessionManager
{
    IReadOnlyList<Message> 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<bool> TryCompactAsync(CancellationToken cancellationToken);
    void Reset();
    
    event EventHandler<ContextThresholdEventArgs> ContextThresholdReached;
    event EventHandler<CompactionCompletedEventArgs> CompactionCompleted;
}

public class ChatSessionManager : IChatSessionManager
{
    private readonly List<Message> _history = new();
    private readonly ITokenTracker _tokenTracker;
    private readonly IContextStrategy _strategy;
    private readonly IEventMultiplexer _events;
    
    public IReadOnlyList<Message> History => _history.AsReadOnly();
    public int TokenCount => _tokenTracker.TotalTokens;
    public bool IsContextNearLimit => _strategy.ShouldCompact(_tokenTracker.TotalTokens);
    
    public async Task<bool> 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

public interface IContextStrategy
{
    bool ShouldCompact(int currentTokenCount);
    Task<bool> CompactAsync(List<Message> 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<bool> CompactAsync(List<Message> 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<bool> CompactAsync(List<Message> history, CancellationToken cancellationToken)
    {
        // More aggressive: remove more history, summarize more
    }
}

InputProcessor

Purpose: Routes input between commands and chat

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

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

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

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

public interface IEventMultiplexer
{
    void Subscribe<T>(EventHandler<T> handler) where T : AnchorEvent;
    void Unsubscribe<T>(EventHandler<T> handler) where T : AnchorEvent;
    void Raise<T>(T @event) where T : AnchorEvent;
}

public class EventMultiplexer : IEventMultiplexer
{
    private readonly Dictionary<Type, List<Delegate>> _subscribers = new();
    private readonly object _lock = new();
    
    public void Subscribe<T>(EventHandler<T> handler) where T : AnchorEvent
    {
        lock (_lock)
        {
            if (!_subscribers.ContainsKey(typeof(T)))
            {
                _subscribers[typeof(T)] = new List<Delegate>();
            }
            _subscribers[typeof(T)].Add(handler);
        }
    }
    
    public void Raise<T>(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

// 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<string, object> Arguments { get; }
    public ToolExecutingEvent(string toolName, Dictionary<string, object> 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

public class ToolOutputRenderer : IToolOutputRenderer
{
    private readonly IEventMultiplexer _events;
    private readonly IConsole _console;
    
    public ToolOutputRenderer(IEventMultiplexer events, IConsole console)
    {
        _events = events;
        _console = console;
        _events.Subscribe<ToolExecutingEvent>(OnToolExecuting);
        _events.Subscribe<ToolCompletedEvent>(OnToolCompleted);
        _events.Subscribe<ToolFailedEvent>(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