Skip to content

Commit 9272c52

Browse files
nikomatsakisclaude
andcommitted
feat(tools): create cli::tool module with pure permission logic
- Create crates/chat-cli/src/cli/tool/ module - Add tool::permission::evaluate_tool_permissions() - Add ToolPermissionResult enum for structured data - Extract pure permission logic from chat::tool_use_execute() - Keep all console UI logic in chat module - No behavior changes - identical console output and flow This creates a reusable foundation for tool permission evaluation that can be shared between chat and ACP contexts while maintaining full backward compatibility. Includes TOOL_USE_REFACTOR.md documenting the full refactoring plan and updates to PR_PLAN.md for Phase 3 integration strategy. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a91104e commit 9272c52

File tree

6 files changed

+488
-87
lines changed

6 files changed

+488
-87
lines changed

PR_PLAN.md

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,19 @@ The implementation uses an actor-based pattern for clean message passing instead
3939
- *Dependencies*: Requires MockLLM refactor completion
4040
- *Test*: ACP actor tests work with new stateless MockLLMContext API
4141

42-
### Phase 3: Advanced Features
43-
10. **Tool system integration** - Implement ACP tool permissions and execution through actors
44-
- *Test*: Tools work through session actors, permission requests flow correctly
45-
- *Note*: Session actors handle tool execution, report via ACP `ToolCall` messages
46-
11. **File operation routing** - Replace builtin file tools with ACP versions in session actors
47-
- *Test*: `fs_read`/`fs_write` work through editor, session actors route file operations
42+
### Phase 3: Tool System Refactoring & ACP Integration
43+
10. **Tool system UI separation** - Refactor existing tool system to separate UI concerns from core logic
44+
- *Problem*: Current tool execution mixes permission evaluation, console I/O, and state management
45+
- *Solution*: Extract pure permission functions and create `PermissionInterface` trait abstraction
46+
- *Test*: Identical console behavior with cleaner, more testable architecture
47+
- *Details*: See `TOOL_USE_REFACTOR.md` for complete refactoring plan
48+
11. **ACP tool permissions** - Implement ACP permission interface using refactored system
49+
- *Implementation*: Create `AcpPermissionInterface` that sends protocol permission requests
50+
- *Test*: Tools work through session actors, permission requests flow correctly via ACP
51+
- *Note*: Session actors route tool permissions through ACP instead of console prompts
52+
12. **ACP file operation routing** - Implement ACP file tools using refactored architecture
53+
- *Implementation*: Create ACP-aware `fs_read`/`fs_write` tools that use protocol operations
54+
- *Test*: File operations work through editor, session actors route via ACP protocol
4855
- *Note*: Session actors use ACP file operations instead of direct filesystem access
4956

5057
**Architecture Benefits:**
@@ -115,23 +122,30 @@ ctx.try_patterns(&[
115122
]).await
116123
```
117124

118-
**Phase 3: Advanced Features**
119-
10. ⚠️ **Tool system integration** - Basic tool execution works, need ACP permissions
120-
- Current: Tool use shows as `[Tool execution]` placeholder
125+
**🔄 NEXT - Phase 3: Tool System Refactoring & ACP Integration**
126+
10. **Tool system UI separation** - Ready to begin refactoring
127+
- Current: Tool execution mixes permission evaluation, console I/O, and state management in `tool_use_execute()`
128+
- Plan: Extract pure permission functions, create `PermissionInterface` trait for swappable UI implementations
129+
- Goal: Enable ACP integration while preserving identical console behavior
130+
11. ⚠️ **ACP tool permissions** - Blocked on refactoring completion
131+
- Current: Tool use shows as `[Tool execution]` placeholder in ACP sessions
121132
- Missing: ACP `session/request_permission` flow, proper `ToolCall` messages
122-
11. ⚠️ **File operation routing** - Need ACP file operations instead of direct filesystem
123-
- Current: Uses direct filesystem access
124-
- Missing: Route `fs_read`/`fs_write` through ACP protocol
133+
- Depends: Requires `PermissionInterface` abstraction from step 10
134+
12. ⚠️ **ACP file operation routing** - Blocked on permission system
135+
- Current: Uses direct filesystem access even in ACP sessions
136+
- Missing: Route `fs_read`/`fs_write` through ACP protocol operations
137+
- Depends: Requires ACP tool permission system from step 11
125138

126139
**Minor TODOs:**
127140
- Session configuration from ACP (currently uses defaults)
128-
- Cancel operations implementation (currently no-op)
129141
- Set session mode implementation (currently returns method not found)
130142

131-
**Current State:** The ACP server is **functionally complete** for basic chat functionality with **sophisticated test infrastructure**. Users can connect editors, create sessions, send prompts, and receive streaming AI responses. The actor architecture is solid and now has a stateless MockLLM system that matches real LLM behavior patterns, enabling comprehensive testing of the actor-based system with declarative pattern matching APIs.
143+
**Current State:** The ACP server is **functionally complete** for basic chat functionality with **sophisticated test infrastructure** and **comprehensive cancellation support**. Users can connect editors, create sessions, send prompts, receive streaming AI responses, and cancel active prompts. The actor architecture is solid and now has a stateless MockLLM system that matches real LLM behavior patterns, enabling comprehensive testing of the actor-based system with declarative pattern matching APIs.
132144

133145
**🔍 READY FOR REVIEW**
134-
The MockLLM refactor work is complete and ready for in-depth code review. Key areas for review:
146+
The ACP server implementation is complete and ready for in-depth code review. Key areas for review:
147+
- **Cancellation system** (`crates/chat-cli/src/cli/acp/server_session.rs`) - Concurrent prompt processing with tokio::select! cancellation
148+
- **Cross-session isolation** (`crates/chat-cli/src/cli/acp/tests.rs`) - Comprehensive test coverage for session independence
135149
- **MockLLM architecture** (`crates/chat-cli/src/mock_llm.rs`) - Stateless per-turn design with streaming
136150
- **Pattern matching API** - Regex-based conversation matching with declarative `try_patterns()`
137151
- **ApiClient integration** - Streaming compatibility and performance fixes

TOOL_USE_REFACTOR.md

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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

Comments
 (0)