1102 lines
31 KiB
Markdown
1102 lines
31 KiB
Markdown
|
||
---
|
||
|
||
## 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<Message>();
|
||
var tokenTracker = new TokenTracker();
|
||
var compactor = new ContextCompactor();
|
||
```
|
||
|
||
### ContextCompactor.cs (Half-Static, Half-Instance)
|
||
|
||
**Inconsistent design:**
|
||
```csharp
|
||
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
|
||
|
||
```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<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):**
|
||
```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<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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```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<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
|
||
|
||
```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<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
|
||
|
||
```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<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*
|