Add plugin install extension executor

This commit is contained in:
Zhanshi Wang
2026-06-17 16:59:17 -07:00
parent e45aefe0d9
commit a97eeeec13
2 changed files with 134 additions and 0 deletions
+26
View File
@@ -1,10 +1,15 @@
mod domain;
mod spec;
mod tool;
mod validation;
pub const REQUEST_PLUGIN_INSTALLS_TOOL_NAME: &str = "request_plugin_installs";
pub(crate) const MAX_REQUEST_PLUGIN_INSTALLS_ENTRIES: usize = 16;
use std::future::Future;
use std::pin::Pin;
use codex_extension_api::FunctionCallError;
pub use domain::REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE;
pub use domain::REQUEST_PLUGIN_INSTALL_PERSIST_KEY;
pub use domain::RequestPluginInstallEntryResult;
@@ -20,4 +25,25 @@ pub use domain::verified_connector_install_completed;
pub use spec::ToolSuggestPresentation;
pub use spec::create_request_plugin_installs_tool;
pub use spec::create_request_plugin_installs_tool_for_tui;
pub use tool::RequestPluginInstallsMode;
pub use tool::request_plugin_installs_tool;
pub use validation::request_plugin_install_picker_completed;
pub type RequestPluginInstallsBackendFuture<'a> = Pin<
Box<dyn Future<Output = Result<RequestPluginInstallsResult, FunctionCallError>> + Send + 'a>,
>;
pub struct RequestPluginInstallsRequest {
pub call_id: String,
pub turn_id: String,
pub args: RequestPluginInstallsArgs,
pub resolved_entries: Vec<RequestPluginInstallResolvedPickerEntry>,
}
/// Host operations needed to complete a plugin install suggestion.
pub trait RequestPluginInstallsBackend: Send + Sync {
fn execute(
&self,
request: RequestPluginInstallsRequest,
) -> RequestPluginInstallsBackendFuture<'_>;
}
+108
View File
@@ -0,0 +1,108 @@
use std::sync::Arc;
use codex_extension_api::FunctionCallError;
use codex_extension_api::JsonToolOutput;
use codex_extension_api::ToolCall;
use codex_extension_api::ToolExecutor;
use codex_extension_api::ToolName;
use codex_extension_api::ToolOutput;
use codex_extension_api::ToolSpec;
use codex_tools::DiscoverableTool;
use codex_tools::DiscoverableToolAction;
use crate::REQUEST_PLUGIN_INSTALLS_TOOL_NAME;
use crate::RequestPluginInstallsArgs;
use crate::RequestPluginInstallsBackend;
use crate::RequestPluginInstallsRequest;
use crate::ToolSuggestPresentation;
use crate::spec::create_request_plugin_installs_tool;
use crate::spec::create_request_plugin_installs_tool_for_tui;
use crate::validation::validate_request_plugin_install_picker_args;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestPluginInstallsMode {
SingleEntry,
MultipleEntries,
}
struct RequestPluginInstallsTool {
backend: Arc<dyn RequestPluginInstallsBackend>,
discoverable_tools: Vec<DiscoverableTool>,
presentation: ToolSuggestPresentation,
mode: RequestPluginInstallsMode,
}
pub fn request_plugin_installs_tool(
backend: Arc<dyn RequestPluginInstallsBackend>,
discoverable_tools: Vec<DiscoverableTool>,
presentation: ToolSuggestPresentation,
mode: RequestPluginInstallsMode,
) -> Arc<dyn ToolExecutor<ToolCall>> {
Arc::new(RequestPluginInstallsTool {
backend,
discoverable_tools,
presentation,
mode,
})
}
impl ToolExecutor<ToolCall> for RequestPluginInstallsTool {
fn tool_name(&self) -> ToolName {
ToolName::plain(REQUEST_PLUGIN_INSTALLS_TOOL_NAME)
}
fn spec(&self) -> ToolSpec {
match self.mode {
RequestPluginInstallsMode::SingleEntry => {
create_request_plugin_installs_tool_for_tui(self.presentation)
}
RequestPluginInstallsMode::MultipleEntries => {
create_request_plugin_installs_tool(self.presentation)
}
}
}
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_> {
Box::pin(self.handle_call(call))
}
}
impl RequestPluginInstallsTool {
async fn handle_call(&self, call: ToolCall) -> Result<Box<dyn ToolOutput>, FunctionCallError> {
let arguments = call.function_arguments()?;
let args: RequestPluginInstallsArgs = serde_json::from_str(arguments).map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to parse function arguments: {err}"))
})?;
if args.action_type != DiscoverableToolAction::Install {
return Err(FunctionCallError::RespondToModel(
"plugin install requests currently support only action_type=\"install\""
.to_string(),
));
}
let client_name = match self.mode {
RequestPluginInstallsMode::SingleEntry => Some("codex-tui"),
RequestPluginInstallsMode::MultipleEntries => None,
};
let resolved_entries = validate_request_plugin_install_picker_args(
&args,
&self.discoverable_tools,
client_name,
self.presentation,
)?;
let result = self
.backend
.execute(RequestPluginInstallsRequest {
call_id: call.call_id,
turn_id: call.turn_id,
args,
resolved_entries,
})
.await?;
let value = serde_json::to_value(result).map_err(|err| {
FunctionCallError::Fatal(format!(
"failed to serialize {REQUEST_PLUGIN_INSTALLS_TOOL_NAME} response: {err}"
))
})?;
Ok(Box::new(JsonToolOutput::with_success(value, Some(true))))
}
}