|
| 1 | +# Tool Use System Refactoring Plan |
| 2 | + |
| 3 | +This document outlines the refactoring of the tool use system to separate UI concerns from core logic, enabling clean ACP integration without affecting existing console functionality. |
| 4 | + |
| 5 | +## Problem Statement |
| 6 | + |
| 7 | +The current tool execution system in `crates/chat-cli/src/cli/chat/mod.rs` has three concerns tightly coupled together: |
| 8 | + |
| 9 | +1. **Permission Evaluation** - Pure logic determining if tools are allowed |
| 10 | +2. **UI Interaction** - Console output, user prompts, colored formatting |
| 11 | +3. **Flow Control** - State machine with `ChatState::PromptUser` and `pending_tool_index` |
| 12 | + |
| 13 | +This coupling creates several issues: |
| 14 | +- **ACP Integration Blocker**: Console I/O prevents use in protocol-based sessions |
| 15 | +- **Complex State Machine**: Scattered control flow across multiple state transitions |
| 16 | +- **Testing Difficulty**: UI side effects make unit testing challenging |
| 17 | +- **Code Clarity**: Mixed concerns make the logic hard to follow |
| 18 | + |
| 19 | +## Current Architecture Issues |
| 20 | + |
| 21 | +### Console I/O Coupling |
| 22 | +```rust |
| 23 | +// Lines 2280-2291 in tool_use_execute() |
| 24 | +execute!( |
| 25 | + self.stderr, |
| 26 | + style::SetForegroundColor(Color::Red), |
| 27 | + style::Print("Command "), |
| 28 | + style::Print(&tool.name), |
| 29 | + style::Print(" is rejected..."), |
| 30 | +)?; |
| 31 | +``` |
| 32 | + |
| 33 | +### State Machine Complexity |
| 34 | +```rust |
| 35 | +// Permission needed → return special state |
| 36 | +self.pending_tool_index = Some(i); |
| 37 | +return Ok(ChatState::PromptUser { skip_printing_tools: false }); |
| 38 | + |
| 39 | +// Later, somehow resume from pending state... |
| 40 | +``` |
| 41 | + |
| 42 | +### Mixed Concerns in Single Method |
| 43 | +The `tool_use_execute()` method handles: |
| 44 | +- Permission evaluation logic |
| 45 | +- Terminal formatting and output |
| 46 | +- User input handling |
| 47 | +- Tool execution |
| 48 | +- State management |
| 49 | + |
| 50 | +## Refactoring Solution |
| 51 | + |
| 52 | +### Architecture Overview |
| 53 | + |
| 54 | +``` |
| 55 | +┌─────────────────────────────────────────────────────────────┐ |
| 56 | +│ tool_use_execute() │ |
| 57 | +│ (Clean async flow) │ |
| 58 | +└─────────────────┬───────────────────────────────────────────┘ |
| 59 | + │ |
| 60 | +┌─────────────────▼───────────────────────────────────────────┐ |
| 61 | +│ Permission Evaluation │ |
| 62 | +│ (Pure functions) │ |
| 63 | +└─────────────────┬───────────────────────────────────────────┘ |
| 64 | + │ |
| 65 | +┌─────────────────▼───────────────────────────────────────────┐ |
| 66 | +│ PermissionInterface │ |
| 67 | +│ (Async trait) │ |
| 68 | +└─────────────┬─────────────────────────────┬─────────────────┘ |
| 69 | + │ │ |
| 70 | +┌─────────────▼─────────────┐ ┌─────────▼─────────────────┐ |
| 71 | +│ ConsolePermissionInterface│ │ AcpPermissionInterface │ |
| 72 | +│ (Terminal I/O) │ │ (Protocol messages) │ |
| 73 | +└────────────────────────────┘ └───────────────────────────┘ |
| 74 | +``` |
| 75 | + |
| 76 | +### Key Components |
| 77 | + |
| 78 | +#### 1. Pure Permission Evaluation |
| 79 | +```rust |
| 80 | +// New file: crates/chat-cli/src/cli/chat/permission.rs |
| 81 | +pub fn evaluate_tool_permissions( |
| 82 | + tools: &[QueuedTool], |
| 83 | + agents: &AgentState, |
| 84 | + os: &Os, |
| 85 | +) -> Vec<ToolPermissionResult>; |
| 86 | + |
| 87 | +pub enum ToolPermissionResult { |
| 88 | + Allowed, |
| 89 | + RequiresConfirmation { tool_index: usize, tool_name: String }, |
| 90 | + Denied { tool_index: usize, rules: Vec<String> }, |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +#### 2. Permission Interface Abstraction |
| 95 | +```rust |
| 96 | +#[async_trait] |
| 97 | +pub trait PermissionInterface { |
| 98 | + async fn request_permission( |
| 99 | + &mut self, |
| 100 | + tool: &QueuedTool, |
| 101 | + context: &PermissionContext, |
| 102 | + ) -> Result<PermissionDecision>; |
| 103 | + |
| 104 | + async fn show_denied_tool( |
| 105 | + &mut self, |
| 106 | + tool: &QueuedTool, |
| 107 | + rules: Vec<String>, |
| 108 | + ) -> Result<()>; |
| 109 | + |
| 110 | + async fn show_tool_execution( |
| 111 | + &mut self, |
| 112 | + tool: &QueuedTool, |
| 113 | + allowed: bool, |
| 114 | + ) -> Result<()>; |
| 115 | +} |
| 116 | + |
| 117 | +pub enum PermissionDecision { |
| 118 | + Approved, |
| 119 | + Rejected, |
| 120 | + Cancelled, |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +#### 3. Console Implementation |
| 125 | +```rust |
| 126 | +// New file: crates/chat-cli/src/cli/chat/permission/console.rs |
| 127 | +pub struct ConsolePermissionInterface<'a> { |
| 128 | + stdout: &'a mut dyn Write, |
| 129 | + stderr: &'a mut dyn Write, |
| 130 | + stdin: Box<dyn BufRead>, |
| 131 | +} |
| 132 | + |
| 133 | +#[async_trait] |
| 134 | +impl PermissionInterface for ConsolePermissionInterface<'_> { |
| 135 | + async fn request_permission(&mut self, tool: &QueuedTool, context: &PermissionContext) -> Result<PermissionDecision> { |
| 136 | + // Existing console behavior: |
| 137 | + // - Show colored tool description |
| 138 | + // - Play notification bell |
| 139 | + // - Read user input from stdin |
| 140 | + // - Return decision |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +#### 4. Refactored Main Flow |
| 146 | +```rust |
| 147 | +async fn tool_use_execute(&mut self, os: &mut Os) -> Result<ChatState, ChatError> { |
| 148 | + // 1. Pure permission evaluation |
| 149 | + let permission_results = evaluate_tool_permissions(&self.tool_uses, &self.conversation.agents, os); |
| 150 | + |
| 151 | + // 2. Create appropriate permission interface |
| 152 | + let mut permission_interface = self.create_permission_interface(); |
| 153 | + |
| 154 | + // 3. Handle permissions with clean async flow |
| 155 | + for result in permission_results { |
| 156 | + match result { |
| 157 | + ToolPermissionResult::RequiresConfirmation { tool_index, .. } => { |
| 158 | + let tool = &self.tool_uses[tool_index]; |
| 159 | + let decision = permission_interface.request_permission(tool, &context).await?; |
| 160 | + match decision { |
| 161 | + PermissionDecision::Approved => { |
| 162 | + self.tool_uses[tool_index].accepted = true; |
| 163 | + } |
| 164 | + PermissionDecision::Rejected => { |
| 165 | + return Ok(ChatState::HandleInput { |
| 166 | + input: format!("Tool {} was rejected", tool.name) |
| 167 | + }); |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + ToolPermissionResult::Denied { tool_index, rules } => { |
| 172 | + let tool = &self.tool_uses[tool_index]; |
| 173 | + permission_interface.show_denied_tool(tool, rules).await?; |
| 174 | + return Ok(ChatState::HandleInput { |
| 175 | + input: format!("Tool {} was denied", tool.name) |
| 176 | + }); |
| 177 | + } |
| 178 | + ToolPermissionResult::Allowed => { |
| 179 | + // Tool already approved |
| 180 | + } |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + // 4. Execute tools (existing logic, unchanged) |
| 185 | + self.execute_approved_tools(os).await |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +## Benefits |
| 190 | + |
| 191 | +### 1. Eliminates State Machine Complexity |
| 192 | +- **Before**: Complex state transitions with `ChatState::PromptUser` and `pending_tool_index` |
| 193 | +- **After**: Straightforward async flow that reads naturally top-to-bottom |
| 194 | + |
| 195 | +### 2. Enables Clean ACP Integration |
| 196 | +- **Console Interface**: Preserves existing terminal behavior exactly |
| 197 | +- **ACP Interface**: Routes permission requests through protocol messages |
| 198 | +- **Swappable**: Same core logic, different UI implementations |
| 199 | + |
| 200 | +### 3. Improves Testability |
| 201 | +- **Pure Functions**: Permission evaluation can be unit tested without UI |
| 202 | +- **Mockable Interfaces**: Permission interfaces can be mocked for testing |
| 203 | +- **Isolated Concerns**: Each component can be tested independently |
| 204 | + |
| 205 | +### 4. Maintains Backward Compatibility |
| 206 | +- **Zero Behavior Changes**: Existing console functionality identical |
| 207 | +- **Same External API**: No changes to public interfaces |
| 208 | +- **Incremental Migration**: Can be implemented step-by-step |
| 209 | + |
| 210 | +## Implementation Plan |
| 211 | + |
| 212 | +### Phase 1: Extract Permission Evaluation |
| 213 | +1. Create `crates/chat-cli/src/cli/chat/permission.rs` |
| 214 | +2. Move permission logic from `tool_use_execute()` into pure functions |
| 215 | +3. Define `ToolPermissionResult` enum for structured results |
| 216 | +4. Update `tool_use_execute()` to use extracted functions |
| 217 | + |
| 218 | +### Phase 2: Create Permission Interface |
| 219 | +1. Define `PermissionInterface` trait with async methods |
| 220 | +2. Create `PermissionContext` for passing context data |
| 221 | +3. Define `PermissionDecision` enum for results |
| 222 | + |
| 223 | +### Phase 3: Implement Console Interface |
| 224 | +1. Create `crates/chat-cli/src/cli/chat/permission/console.rs` |
| 225 | +2. Move existing console I/O logic into `ConsolePermissionInterface` |
| 226 | +3. Implement all trait methods preserving current behavior |
| 227 | +4. Handle stdin reading asynchronously |
| 228 | + |
| 229 | +### Phase 4: Refactor Main Flow |
| 230 | +1. Update `tool_use_execute()` to use new interfaces |
| 231 | +2. Add `create_permission_interface()` factory method |
| 232 | +3. Remove old permission handling code |
| 233 | +4. Remove `pending_tool_index` and related state machine logic |
| 234 | + |
| 235 | +### Phase 5: Testing and Validation |
| 236 | +1. Add unit tests for permission evaluation functions |
| 237 | +2. Add integration tests for console interface |
| 238 | +3. Verify identical behavior with existing system |
| 239 | +4. Performance testing to ensure no regressions |
| 240 | + |
| 241 | +## Future ACP Integration |
| 242 | + |
| 243 | +Once this refactoring is complete, ACP integration becomes straightforward: |
| 244 | + |
| 245 | +```rust |
| 246 | +// Future ACP implementation |
| 247 | +pub struct AcpPermissionInterface { |
| 248 | + client: AcpClientHandle, |
| 249 | + session_id: SessionId, |
| 250 | +} |
| 251 | + |
| 252 | +#[async_trait] |
| 253 | +impl PermissionInterface for AcpPermissionInterface { |
| 254 | + async fn request_permission(&mut self, tool: &QueuedTool, context: &PermissionContext) -> Result<PermissionDecision> { |
| 255 | + // Send protocol message |
| 256 | + let request = acp::RequestPermissionRequest { |
| 257 | + tool_name: tool.name.clone(), |
| 258 | + tool_args: tool.tool.get_args(), |
| 259 | + reason: context.reason.clone(), |
| 260 | + }; |
| 261 | + |
| 262 | + let response = self.client.request_permission(request).await?; |
| 263 | + |
| 264 | + Ok(match response.decision { |
| 265 | + acp::PermissionResult::Approved => PermissionDecision::Approved, |
| 266 | + acp::PermissionResult::Denied => PermissionDecision::Rejected, |
| 267 | + }) |
| 268 | + } |
| 269 | +} |
| 270 | +``` |
| 271 | + |
| 272 | +The ACP session actor can then create an `AcpPermissionInterface` instead of `ConsolePermissionInterface`, routing all permission requests through the protocol without changing any core tool logic. |
| 273 | + |
| 274 | +## File Structure |
| 275 | + |
| 276 | +``` |
| 277 | +crates/chat-cli/src/cli/chat/ |
| 278 | +├── mod.rs # Updated tool_use_execute() |
| 279 | +├── permission.rs # Pure permission evaluation |
| 280 | +└── permission/ |
| 281 | + ├── mod.rs # PermissionInterface trait |
| 282 | + ├── console.rs # Console implementation |
| 283 | + └── acp.rs # Future ACP implementation |
| 284 | +``` |
| 285 | + |
| 286 | +## Risk Mitigation |
| 287 | + |
| 288 | +### Behavior Preservation |
| 289 | +- **Identical Console Flow**: All existing terminal interactions preserved exactly |
| 290 | +- **Same Error Messages**: Error formatting and messaging unchanged |
| 291 | +- **Performance Parity**: No significant performance impact |
| 292 | + |
| 293 | +### Testing Strategy |
| 294 | +- **Side-by-Side Testing**: Run old and new implementations in parallel |
| 295 | +- **Integration Tests**: Full tool execution scenarios |
| 296 | +- **Manual Verification**: Interactive testing of permission flows |
| 297 | + |
| 298 | +### Rollback Plan |
| 299 | +- **Incremental Changes**: Each phase can be implemented and tested independently |
| 300 | +- **Feature Flags**: Can gate new implementation behind feature flag if needed |
| 301 | +- **Clean Separation**: Old code can be preserved during transition |
| 302 | + |
| 303 | +This refactoring provides a clean foundation for ACP tool integration while maintaining full backward compatibility with existing console-based tool execution. |
0 commit comments