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:
parent
c01ae72e30
commit
00718b4c3e
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
Loading…
Reference in New Issue