refactor(main): extract tool_executor.rs — CliToolExecutor and convert_messages

- 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
This commit is contained in:
zhaoyanchao 2026-05-26 22:01:07 +08:00
parent c01ae72e30
commit 00718b4c3e
3 changed files with 223 additions and 28 deletions

View File

@ -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

View File

@ -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<usize>,
}
#[derive(Debug, Deserialize)]
struct McpToolRequest {
#[serde(rename = "qualifiedName")]
qualified_name: Option<String>,
tool: Option<String>,
arguments: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct ListMcpResourcesRequest {
server: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ReadMcpResourceRequest {
server: String,
uri: String,
}
impl RuntimeMcpState {
fn new(
runtime_config: &runtime::RuntimeConfig,

View File

@ -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<usize>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct McpToolRequest {
#[serde(rename = "qualifiedName")]
pub(crate) qualified_name: Option<String>,
pub(crate) tool: Option<String>,
pub(crate) arguments: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ListMcpResourcesRequest {
pub(crate) server: Option<String>,
}
#[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<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
}
impl CliToolExecutor {
pub(crate) fn new(
allowed_tools: Option<AllowedToolSet>,
emit_output: bool,
tool_registry: GlobalToolRegistry,
mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
) -> Self {
Self {
renderer: TerminalRenderer::new(),
emit_output,
allowed_tools,
tool_registry,
mcp_state,
}
}
fn execute_search_tool(&self, value: serde_json::Value) -> Result<String, ToolError> {
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<String, ToolError> {
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<String, ToolError> {
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<InputMessage> {
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::<Vec<_>>();
(!content.is_empty()).then(|| InputMessage {
role: role.to_string(),
content,
})
})
.collect()
}