Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,9 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
tool_suggest_candidates: None,
mcp_tools: None,
deferred_mcp_tools: None,
discoverable_tools: None,
extension_tool_executors: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
Expand Down Expand Up @@ -9600,9 +9600,9 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
let router = ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools,
mcp_tools: Some(tools),
discoverable_tools: None,
extension_tool_executors: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
Expand Down
11 changes: 8 additions & 3 deletions codex-rs/core/src/session/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::parallel::ToolCallRuntime;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::router::ToolRouterParams;
use crate::tools::router::ToolSuggestCandidates;
use crate::tools::router::ToolSuggestPresentation;
use crate::tools::router::extension_tool_executors;
use crate::tools::spec_plan::search_tool_enabled;
use crate::tools::spec_plan::tool_suggest_enabled;
Expand Down Expand Up @@ -1190,7 +1192,7 @@ pub(crate) async fn built_tools(
.into_iter()
.map(|connector_id| connector_id.0)
.collect::<Vec<_>>();
let discoverable_tools = async {
let tool_suggest_candidates = async {
if apps_enabled && tool_suggest_enabled(turn_context) {
if let Some(accessible_connectors) = accessible_connectors_with_enabled_state.as_ref() {
match connectors::list_tool_suggest_discoverable_tools_with_auth(
Expand All @@ -1208,7 +1210,10 @@ pub(crate) async fn built_tools(
)
}) {
Ok(discoverable_tools) if discoverable_tools.is_empty() => None,
Ok(discoverable_tools) => Some(discoverable_tools),
Ok(discoverable_tools) => Some(ToolSuggestCandidates {
tools: discoverable_tools,
presentation: ToolSuggestPresentation::ListTool,
}),
Err(err) => {
warn!("failed to load discoverable tool suggestions: {err:#}");
None
Expand Down Expand Up @@ -1237,7 +1242,7 @@ pub(crate) async fn built_tools(
ToolRouterParams {
mcp_tools,
deferred_mcp_tools,
discoverable_tools,
tool_suggest_candidates,
extension_tool_executors: extension_tool_executors(sess),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/stream_events_utils_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,9 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() {
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
tool_suggest_candidates: None,
mcp_tools: None,
deferred_mcp_tools: None,
discoverable_tools: None,
extension_tool_executors: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
Expand Down
24 changes: 20 additions & 4 deletions codex-rs/core/src/tools/handlers/request_plugin_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,22 @@ use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use crate::tools::router::ToolSuggestPresentation;

pub struct RequestPluginInstallHandler {
discoverable_tools: Vec<DiscoverableTool>,
presentation: ToolSuggestPresentation,
}

impl RequestPluginInstallHandler {
pub(crate) fn new(discoverable_tools: Vec<DiscoverableTool>) -> Self {
Self { discoverable_tools }
pub(crate) fn new(
discoverable_tools: Vec<DiscoverableTool>,
presentation: ToolSuggestPresentation,
) -> Self {
Self {
discoverable_tools,
presentation,
}
}
}

Expand All @@ -54,7 +62,7 @@ impl ToolExecutor<ToolInvocation> for RequestPluginInstallHandler {
}

fn spec(&self) -> ToolSpec {
create_request_plugin_install_tool()
create_request_plugin_install_tool(self.presentation)
}

fn supports_parallel_tool_calls(&self) -> bool {
Expand Down Expand Up @@ -118,8 +126,16 @@ impl RequestPluginInstallHandler {
.into_iter()
.find(|tool| tool.tool_type() == args.tool_type && tool.id() == args.tool_id)
.ok_or_else(|| {
let source = match self.presentation {
ToolSuggestPresentation::ListTool => format!(
"the discoverable tools returned by {LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME}"
),
ToolSuggestPresentation::DeveloperContext => {
"the developer recommendations".to_string()
}
};
FunctionCallError::RespondToModel(format!(
"tool_id must match one of the discoverable tools returned by {LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME}"
"tool_id must match one of {source}"
))
})?;

Expand Down
34 changes: 28 additions & 6 deletions codex-rs/core/src/tools/handlers/request_plugin_install_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ use codex_tools::ResponsesApiTool;
use codex_tools::ToolSpec;
use std::collections::BTreeMap;

pub(crate) fn create_request_plugin_install_tool() -> ToolSpec {
use crate::tools::router::ToolSuggestPresentation;

pub(crate) fn create_request_plugin_install_tool(
presentation: ToolSuggestPresentation,
) -> ToolSpec {
let properties = BTreeMap::from([
(
"tool_type".to_string(),
Expand All @@ -31,9 +35,13 @@ pub(crate) fn create_request_plugin_install_tool() -> ToolSpec {
),
]);

let description = format!(
"# Request plugin/connector install\n\nUse this tool only after `{LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME}` returns a plugin or connector that exactly matches the user's explicit request.\n\nDo not use it for adjacent capabilities, broad recommendations, or tools that merely seem useful. Pass the returned `tool_type` through directly, and pass the returned `id` as `tool_id`.\n\nIMPORTANT: DO NOT call this tool in parallel with other tools."
);
let description = match presentation {
ToolSuggestPresentation::ListTool => format!(
"# Request plugin/connector install\n\nUse this tool only after `{LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME}` returns a plugin or connector that exactly matches the user's explicit request.\n\nDo not use it for adjacent capabilities, broad recommendations, or tools that merely seem useful. Pass the returned `tool_type` through directly, and pass the returned `id` as `tool_id`.\n\nIMPORTANT: DO NOT call this tool in parallel with other tools."
),
ToolSuggestPresentation::DeveloperContext =>
"# Suggest a recommended plugin installation\n\nSuggest installing a plugin from the developer `<recommended_plugins>` list when it would help with the user's current request. Briefly explain why in `suggest_reason`.".to_string(),
};

ToolSpec::Function(ResponsesApiTool {
name: REQUEST_PLUGIN_INSTALL_TOOL_NAME.to_string(),
Expand Down Expand Up @@ -62,7 +70,7 @@ mod tests {
use std::collections::BTreeMap;

#[test]
fn create_request_plugin_install_tool_uses_expected_wire_shape() {
fn create_request_plugin_install_tool_uses_expected_legacy_wire_shape() {
let expected_description = concat!(
"# Request plugin/connector install\n\n",
"Use this tool only after `list_available_plugins_to_install` returns a plugin or connector that exactly matches the user's explicit request.\n\n",
Expand All @@ -71,7 +79,7 @@ mod tests {
);

assert_eq!(
create_request_plugin_install_tool(),
create_request_plugin_install_tool(ToolSuggestPresentation::ListTool),
ToolSpec::Function(ResponsesApiTool {
name: "request_plugin_install".to_string(),
description: expected_description.to_string(),
Expand Down Expand Up @@ -116,4 +124,18 @@ mod tests {
})
);
}

#[test]
fn developer_recommendations_change_only_the_description() {
let mut expected = create_request_plugin_install_tool(ToolSuggestPresentation::ListTool);
let recommendations =
create_request_plugin_install_tool(ToolSuggestPresentation::DeveloperContext);

let ToolSpec::Function(expected_function) = &mut expected else {
panic!("expected function tool specs");
};
expected_function.description = "# Suggest a recommended plugin installation\n\nSuggest installing a plugin from the developer `<recommended_plugins>` list when it would help with the user's current request. Briefly explain why in `suggest_reason`.".to_string();

assert_eq!(recommendations, expected);
}
}
15 changes: 14 additions & 1 deletion codex-rs/core/src/tools/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,24 @@ pub struct ToolRouter {
pub(crate) struct ToolRouterParams<'a> {
pub(crate) mcp_tools: Option<Vec<ToolInfo>>,
pub(crate) deferred_mcp_tools: Option<Vec<ToolInfo>>,
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
pub(crate) tool_suggest_candidates: Option<ToolSuggestCandidates>,
pub(crate) extension_tool_executors: Vec<Arc<dyn ToolExecutor<ExtensionToolCall>>>,
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ToolSuggestPresentation {
ListTool,
#[allow(dead_code)]
DeveloperContext,
}

#[derive(Clone, Debug)]
pub(crate) struct ToolSuggestCandidates {
Comment on lines +49 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/question-- is this ever going to be for something other than plugins? we renamed the tool from tool_suggest to request_plugin_install, and now we are removing connectors. should the types be more plugin specific?

pub(crate) tools: Vec<DiscoverableTool>,
pub(crate) presentation: ToolSuggestPresentation,
}

impl ToolRouter {
pub(crate) fn from_turn_context(
turn_context: &TurnContext,
Expand Down
10 changes: 5 additions & 5 deletions codex-rs/core/src/tools/router_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
mcp_tools: Some(mcp_tools),
discoverable_tools: None,
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
Expand Down Expand Up @@ -182,6 +182,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
mcp_tools: Some(vec![
mcp_tool_info(
Expand All @@ -197,7 +198,6 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
"query_with_delay",
),
]),
discoverable_tools: None,
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
Expand Down Expand Up @@ -231,9 +231,9 @@ async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()>
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
mcp_tools: None,
discoverable_tools: None,
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
Expand Down Expand Up @@ -286,9 +286,9 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
mcp_tools: None,
discoverable_tools: None,
extension_tool_executors: Vec::new(),
dynamic_tools: &dynamic_tools,
},
Expand Down Expand Up @@ -349,9 +349,9 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
mcp_tools: None,
discoverable_tools: None,
extension_tool_executors: extension_tool_executors(&session),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
Expand Down
23 changes: 13 additions & 10 deletions codex-rs/core/src/tools/spec_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tools::DiscoverableTool;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
Expand Down Expand Up @@ -145,7 +144,7 @@ struct CoreToolPlanContext<'a> {
turn_context: &'a TurnContext,
mcp_tools: Option<&'a [ToolInfo]>,
deferred_mcp_tools: Option<&'a [ToolInfo]>,
discoverable_tools: Option<&'a [DiscoverableTool]>,
tool_suggest_candidates: Option<&'a crate::tools::router::ToolSuggestCandidates>,
extension_tool_executors: &'a [Arc<dyn ToolExecutor<ExtensionToolCall>>],
dynamic_tools: &'a [DynamicToolSpec],
tool_search_handler_cache: &'a ToolSearchHandlerCache,
Expand Down Expand Up @@ -173,7 +172,7 @@ fn build_tool_specs_and_registry(
let ToolRouterParams {
mcp_tools,
deferred_mcp_tools,
discoverable_tools,
tool_suggest_candidates,
extension_tool_executors,
dynamic_tools,
} = params;
Expand All @@ -183,7 +182,7 @@ fn build_tool_specs_and_registry(
turn_context,
mcp_tools: mcp_tools.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
tool_suggest_candidates: tool_suggest_candidates.as_ref(),
extension_tool_executors: &extension_tool_executors,
dynamic_tools,
tool_search_handler_cache,
Expand Down Expand Up @@ -672,14 +671,18 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
}

if tool_suggest_enabled(turn_context)
&& let Some(discoverable_tools) =
context.discoverable_tools.filter(|tools| !tools.is_empty())
&& let Some(candidates) = context
.tool_suggest_candidates
.filter(|candidates| !candidates.tools.is_empty())
{
planned_tools.add(ListAvailablePluginsToInstallHandler::new(
collect_request_plugin_install_entries(discoverable_tools),
));
if candidates.presentation == crate::tools::router::ToolSuggestPresentation::ListTool {
planned_tools.add(ListAvailablePluginsToInstallHandler::new(
collect_request_plugin_install_entries(&candidates.tools),
));
}
planned_tools.add(RequestPluginInstallHandler::new(
discoverable_tools.to_vec(),
candidates.tools.clone(),
candidates.presentation,
));
}

Expand Down
Loading
Loading