diff --git a/codex-rs/core/src/tools/handlers/reload_plugins.rs b/codex-rs/core/src/tools/handlers/reload_plugins.rs new file mode 100644 index 0000000000..7176a991f5 --- /dev/null +++ b/codex-rs/core/src/tools/handlers/reload_plugins.rs @@ -0,0 +1,73 @@ +use crate::function_tool::FunctionCallError; +use crate::tools::context::FunctionToolOutput; +use crate::tools::context::ToolInvocation; +use crate::tools::context::ToolOutput; +use crate::tools::context::ToolPayload; +use crate::tools::context::boxed_tool_output; +use crate::tools::handlers::reload_plugins_spec::RELOAD_PLUGINS_TOOL_NAME; +use crate::tools::handlers::reload_plugins_spec::create_reload_plugins_tool; +use crate::tools::registry::CoreToolRuntime; +use crate::tools::registry::ToolExecutor; +use codex_tools::ToolName; +use codex_tools::ToolSpec; + +pub struct ReloadPluginsHandler; + +#[async_trait::async_trait] +impl ToolExecutor for ReloadPluginsHandler { + fn tool_name(&self) -> ToolName { + ToolName::plain(RELOAD_PLUGINS_TOOL_NAME) + } + + fn spec(&self) -> Option { + Some(create_reload_plugins_tool()) + } + + async fn handle( + &self, + invocation: ToolInvocation, + ) -> Result, FunctionCallError> { + let ToolInvocation { + payload, + session, + turn, + .. + } = invocation; + + match payload { + ToolPayload::Function { arguments } if arguments.trim() == "{}" => {} + ToolPayload::Function { arguments } if arguments.trim().is_empty() => {} + ToolPayload::Function { .. } => { + return Err(FunctionCallError::RespondToModel( + "reload_plugins does not accept arguments".to_string(), + )); + } + _ => { + return Err(FunctionCallError::Fatal(format!( + "{RELOAD_PLUGINS_TOOL_NAME} handler received unsupported payload" + ))); + } + } + + session.reload_user_config_layer().await; + let config = session.get_config().await; + let mcp_config = config + .to_mcp_config(session.plugins_manager().as_ref()) + .await; + session + .refresh_mcp_servers_now( + turn.as_ref(), + mcp_config.configured_mcp_servers.clone(), + mcp_config.mcp_oauth_credentials_store_mode, + Some(session.mcp_elicitation_reviewer()), + ) + .await; + + Ok(boxed_tool_output(FunctionToolOutput::from_text( + "{\"reloaded\":true}".to_string(), + Some(true), + ))) + } +} + +impl CoreToolRuntime for ReloadPluginsHandler {} diff --git a/codex-rs/core/src/tools/handlers/reload_plugins_spec.rs b/codex-rs/core/src/tools/handlers/reload_plugins_spec.rs new file mode 100644 index 0000000000..e42e6a5835 --- /dev/null +++ b/codex-rs/core/src/tools/handlers/reload_plugins_spec.rs @@ -0,0 +1,33 @@ +use codex_tools::JsonSchema; +use codex_tools::ResponsesApiTool; +use codex_tools::ToolSpec; +use std::collections::BTreeMap; + +pub const RELOAD_PLUGINS_TOOL_NAME: &str = "reload_plugins"; + +pub fn create_reload_plugins_tool() -> ToolSpec { + ToolSpec::Function(ResponsesApiTool { + name: RELOAD_PLUGINS_TOOL_NAME.to_string(), + description: "Reload local plugin configuration, clear plugin and skill caches, and rebuild MCP tools from the refreshed plugin state for the current turn. Plugin instructions or skills already injected into an older thread remain unchanged; use a new thread to pick up newly loaded plugin context.".to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(BTreeMap::new(), Some(Vec::new()), Some(false.into())), + output_schema: None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reload_plugins_tool_has_no_arguments() { + let ToolSpec::Function(tool) = create_reload_plugins_tool() else { + panic!("reload_plugins should be a function tool"); + }; + + assert_eq!(tool.name, RELOAD_PLUGINS_TOOL_NAME); + assert_eq!(tool.parameters.required, Some(Vec::new())); + assert_eq!(tool.parameters.properties, Some(BTreeMap::new())); + } +}