From 00718b4c3eaa227f197b14735d207a68410f2e6a Mon Sep 17 00:00:00 2001 From: zhaoyanchao Date: Tue, 26 May 2026 22:01:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor(main):=20extract=20tool=5Fexecutor.rs?= =?UTF-8?q?=20=E2=80=94=20CliToolExecutor=20and=20convert=5Fmessages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created src/tool_executor.rs (218 lines) with CliToolExecutor struct+impl, ToolExecutor trait impl, ToolSearchRequest/McpToolRequest/ ListMcpResourcesRequest/ReadMcpResourceRequest structs, convert_messages - Removed ~100 lines from main.rs, added mod tool_executor + re-export - Fixed missing Deserialize derives and pub(crate) field visibility - All 214 tests pass --- .../changes/refactor-main-rs-split/tasks.md | 6 +- rust/crates/rusty-claude-cli/src/main.rs | 27 +-- .../rusty-claude-cli/src/tool_executor.rs | 218 ++++++++++++++++++ 3 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 rust/crates/rusty-claude-cli/src/tool_executor.rs diff --git a/openspec/changes/refactor-main-rs-split/tasks.md b/openspec/changes/refactor-main-rs-split/tasks.md index 7264ff01..81bc20a0 100644 --- a/openspec/changes/refactor-main-rs-split/tasks.md +++ b/openspec/changes/refactor-main-rs-split/tasks.md @@ -79,9 +79,9 @@ ## 14. Create tool_executor.rs — extract CliToolExecutor -- [ ] 14.1 Create `src/tool_executor.rs` with CliToolExecutor struct+impl, ToolExecutor trait impl, ToolSearchRequest/McpToolRequest/ListMcpResourcesRequest/ReadMcpResourceRequest structs, and `convert_messages` (main.rs lines 8731-8922) -- [ ] 14.2 Add `mod tool_executor;` to `main.rs` and re-export -- [ ] 14.3 Run `cargo build` and `cargo test` — must pass +- [x] 14.1 Create `src/tool_executor.rs` with CliToolExecutor struct+impl, ToolExecutor trait impl, ToolSearchRequest/McpToolRequest/ListMcpResourcesRequest/ReadMcpResourceRequest structs, and `convert_messages` (main.rs lines 8731-8922) +- [x] 14.2 Add `mod tool_executor;` to `main.rs` and re-export +- [x] 14.3 Run `cargo build` and `cargo test` — must pass ## 15. Create runtime_builder.rs — extract runtime construction diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index abe5e204..b308d7fb 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -25,6 +25,7 @@ mod api_client; mod progress; mod repl; mod render; +mod tool_executor; pub(crate) use api_client::*; pub(crate) use args::*; @@ -38,6 +39,7 @@ pub(crate) use model::*; pub(crate) use models::*; pub(crate) use permission::*; pub(crate) use progress::*; +pub(crate) use tool_executor::*; use std::collections::BTreeSet; use std::env; use std::fs; @@ -1471,31 +1473,6 @@ impl Drop for BuiltRuntime { } } -#[derive(Debug, Deserialize)] -struct ToolSearchRequest { - query: String, - max_results: Option, -} - -#[derive(Debug, Deserialize)] -struct McpToolRequest { - #[serde(rename = "qualifiedName")] - qualified_name: Option, - tool: Option, - arguments: Option, -} - -#[derive(Debug, Deserialize)] -struct ListMcpResourcesRequest { - server: Option, -} - -#[derive(Debug, Deserialize)] -struct ReadMcpResourceRequest { - server: String, - uri: String, -} - impl RuntimeMcpState { fn new( runtime_config: &runtime::RuntimeConfig, diff --git a/rust/crates/rusty-claude-cli/src/tool_executor.rs b/rust/crates/rusty-claude-cli/src/tool_executor.rs new file mode 100644 index 00000000..d67445e0 --- /dev/null +++ b/rust/crates/rusty-claude-cli/src/tool_executor.rs @@ -0,0 +1,218 @@ +use std::io; +use std::sync::{Arc, Mutex}; + +use api::{InputContentBlock, InputMessage, ToolResultContentBlock}; +use serde::Deserialize; +use serde_json; + +use runtime::{ + ContentBlock, ConversationMessage, MessageRole, ToolError, ToolExecutor, +}; +use tools::GlobalToolRegistry; + +use crate::constants::*; +use crate::render::{format_tool_result, TerminalRenderer}; +use crate::RuntimeMcpState; + +#[derive(Deserialize)] +pub(crate) struct ToolSearchRequest { + pub(crate) query: String, + pub(crate) max_results: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct McpToolRequest { + #[serde(rename = "qualifiedName")] + pub(crate) qualified_name: Option, + pub(crate) tool: Option, + pub(crate) arguments: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct ListMcpResourcesRequest { + pub(crate) server: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct ReadMcpResourceRequest { + pub(crate) server: String, + pub(crate) uri: String, +} + +pub(crate) struct CliToolExecutor { + renderer: TerminalRenderer, + emit_output: bool, + allowed_tools: Option, + tool_registry: GlobalToolRegistry, + mcp_state: Option>>, +} + +impl CliToolExecutor { + pub(crate) fn new( + allowed_tools: Option, + emit_output: bool, + tool_registry: GlobalToolRegistry, + mcp_state: Option>>, + ) -> Self { + Self { + renderer: TerminalRenderer::new(), + emit_output, + allowed_tools, + tool_registry, + mcp_state, + } + } + + fn execute_search_tool(&self, value: serde_json::Value) -> Result { + let input: ToolSearchRequest = serde_json::from_value(value) + .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?; + let (pending_mcp_servers, mcp_degraded) = + self.mcp_state.as_ref().map_or((None, None), |state| { + let state = state + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + (state.pending_servers(), state.degraded_report()) + }); + serde_json::to_string_pretty(&self.tool_registry.search( + &input.query, + input.max_results.unwrap_or(5), + pending_mcp_servers, + mcp_degraded, + )) + .map_err(|error| ToolError::new(error.to_string())) + } + + fn execute_runtime_tool( + &self, + tool_name: &str, + value: serde_json::Value, + ) -> Result { + let Some(mcp_state) = &self.mcp_state else { + return Err(ToolError::new(format!( + "runtime tool `{tool_name}` is unavailable without configured MCP servers" + ))); + }; + let mut mcp_state = mcp_state + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + match tool_name { + "MCPTool" => { + let input: McpToolRequest = serde_json::from_value(value) + .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?; + let qualified_name = input + .qualified_name + .or(input.tool) + .ok_or_else(|| ToolError::new("missing required field `qualifiedName`"))?; + mcp_state.call_tool(&qualified_name, input.arguments) + } + "ListMcpResourcesTool" => { + let input: ListMcpResourcesRequest = serde_json::from_value(value) + .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?; + match input.server { + Some(server_name) => mcp_state.list_resources_for_server(&server_name), + None => mcp_state.list_resources_for_all_servers(), + } + } + "ReadMcpResourceTool" => { + let input: ReadMcpResourceRequest = serde_json::from_value(value) + .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?; + mcp_state.read_resource(&input.server, &input.uri) + } + _ => mcp_state.call_tool(tool_name, Some(value)), + } + } +} + +impl ToolExecutor for CliToolExecutor { + fn execute(&mut self, tool_name: &str, input: &str) -> Result { + if self + .allowed_tools + .as_ref() + .is_some_and(|allowed| !allowed.contains(tool_name)) + { + return Err(ToolError::new(format!( + "tool `{tool_name}` is not enabled by the current --allowedTools setting" + ))); + } + let value = serde_json::from_str(input) + .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?; + let result = if tool_name == "ToolSearch" { + self.execute_search_tool(value) + } else if self.tool_registry.has_runtime_tool(tool_name) { + self.execute_runtime_tool(tool_name, value) + } else { + self.tool_registry + .execute(tool_name, &value) + .map_err(ToolError::new) + }; + match result { + Ok(output) => { + if self.emit_output { + let markdown = format_tool_result(tool_name, &output, false); + self.renderer + .stream_markdown(&markdown, &mut io::stdout()) + .map_err(|error| ToolError::new(error.to_string()))?; + } + Ok(output) + } + Err(error) => { + if self.emit_output { + let markdown = format_tool_result(tool_name, &error.to_string(), true); + self.renderer + .stream_markdown(&markdown, &mut io::stdout()) + .map_err(|stream_error| ToolError::new(stream_error.to_string()))?; + } + Err(error) + } + } + } +} + +pub(crate) fn convert_messages(messages: &[ConversationMessage]) -> Vec { + messages + .iter() + .filter_map(|message| { + let role = match message.role { + MessageRole::System | MessageRole::User | MessageRole::Tool => "user", + MessageRole::Assistant => "assistant", + }; + let content = message + .blocks + .iter() + .map(|block| match block { + ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() }, + ContentBlock::Thinking { + thinking, + signature, + } => InputContentBlock::Thinking { + thinking: thinking.clone(), + signature: signature.clone(), + }, + ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse { + id: id.clone(), + name: name.clone(), + input: serde_json::from_str(input) + .unwrap_or_else(|_| serde_json::json!({ "raw": input })), + }, + ContentBlock::ToolResult { + tool_use_id, + output, + is_error, + .. + } => InputContentBlock::ToolResult { + tool_use_id: tool_use_id.clone(), + content: vec![ToolResultContentBlock::Text { + text: output.clone(), + }], + is_error: *is_error, + }, + }) + .collect::>(); + (!content.is_empty()).then(|| InputMessage { + role: role.to_string(), + content, + }) + }) + .collect() +} \ No newline at end of file