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. 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)
|
- [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)
|
||||||
- [ ] 14.2 Add `mod tool_executor;` to `main.rs` and re-export
|
- [x] 14.2 Add `mod tool_executor;` to `main.rs` and re-export
|
||||||
- [ ] 14.3 Run `cargo build` and `cargo test` — must pass
|
- [x] 14.3 Run `cargo build` and `cargo test` — must pass
|
||||||
|
|
||||||
## 15. Create runtime_builder.rs — extract runtime construction
|
## 15. Create runtime_builder.rs — extract runtime construction
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ mod api_client;
|
||||||
mod progress;
|
mod progress;
|
||||||
mod repl;
|
mod repl;
|
||||||
mod render;
|
mod render;
|
||||||
|
mod tool_executor;
|
||||||
|
|
||||||
pub(crate) use api_client::*;
|
pub(crate) use api_client::*;
|
||||||
pub(crate) use args::*;
|
pub(crate) use args::*;
|
||||||
|
|
@ -38,6 +39,7 @@ pub(crate) use model::*;
|
||||||
pub(crate) use models::*;
|
pub(crate) use models::*;
|
||||||
pub(crate) use permission::*;
|
pub(crate) use permission::*;
|
||||||
pub(crate) use progress::*;
|
pub(crate) use progress::*;
|
||||||
|
pub(crate) use tool_executor::*;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
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 {
|
impl RuntimeMcpState {
|
||||||
fn new(
|
fn new(
|
||||||
runtime_config: &runtime::RuntimeConfig,
|
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