From 587487df9ec1e8807f2cea7279cd0174a29d3195 Mon Sep 17 00:00:00 2001 From: Alex Daley Date: Tue, 16 Jun 2026 18:44:10 -0400 Subject: [PATCH] [codex] [2/4] Generalize plugin suggestion presentation (#28400) Summary - Add list-backed and developer-context presentations for plugin suggestion candidates. - Let tool planning, install validation, and request-tool copy follow the selected presentation. - Keep every production caller on the existing list-backed presentation, preserving the current list tool, request schema, connector behavior, and model-visible copy. - Leave developer-context presentation latent until the final PR in the stack. Stack - 2/3, based on #28399. - Follow-up: #27704 activates endpoint recommendations. Validation - `just test -p codex-core request_plugin_install` - `just test -p codex-core spec_plan` - `just fix -p codex-core` - `just fmt` - `git diff --check` --- codex-rs/core/src/session/tests.rs | 4 +- codex-rs/core/src/session/turn.rs | 11 +- .../core/src/stream_events_utils_tests.rs | 2 +- .../tools/handlers/request_plugin_install.rs | 24 +++- .../handlers/request_plugin_install_spec.rs | 34 +++++- codex-rs/core/src/tools/router.rs | 15 ++- codex-rs/core/src/tools/router_tests.rs | 10 +- codex-rs/core/src/tools/spec_plan.rs | 23 ++-- codex-rs/core/src/tools/spec_plan_tests.rs | 103 ++++++++++-------- 9 files changed, 147 insertions(+), 79 deletions(-) diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index b4b786c90..6f0e730ca 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -571,9 +571,9 @@ fn test_tool_runtime(session: Arc, turn_context: Arc) -> 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(), }, @@ -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(), }, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index a105aeffe..8eb7d5f9e 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -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; @@ -1190,7 +1192,7 @@ pub(crate) async fn built_tools( .into_iter() .map(|connector_id| connector_id.0) .collect::>(); - 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( @@ -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 @@ -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(), }, diff --git a/codex-rs/core/src/stream_events_utils_tests.rs b/codex-rs/core/src/stream_events_utils_tests.rs index fdb8cddf4..b89d4f5c6 100644 --- a/codex-rs/core/src/stream_events_utils_tests.rs +++ b/codex-rs/core/src/stream_events_utils_tests.rs @@ -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(), }, diff --git a/codex-rs/core/src/tools/handlers/request_plugin_install.rs b/codex-rs/core/src/tools/handlers/request_plugin_install.rs index 55906185a..c4622615d 100644 --- a/codex-rs/core/src/tools/handlers/request_plugin_install.rs +++ b/codex-rs/core/src/tools/handlers/request_plugin_install.rs @@ -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, + presentation: ToolSuggestPresentation, } impl RequestPluginInstallHandler { - pub(crate) fn new(discoverable_tools: Vec) -> Self { - Self { discoverable_tools } + pub(crate) fn new( + discoverable_tools: Vec, + presentation: ToolSuggestPresentation, + ) -> Self { + Self { + discoverable_tools, + presentation, + } } } @@ -54,7 +62,7 @@ impl ToolExecutor 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 { @@ -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}" )) })?; diff --git a/codex-rs/core/src/tools/handlers/request_plugin_install_spec.rs b/codex-rs/core/src/tools/handlers/request_plugin_install_spec.rs index ed143283e..1c9bfec01 100644 --- a/codex-rs/core/src/tools/handlers/request_plugin_install_spec.rs +++ b/codex-rs/core/src/tools/handlers/request_plugin_install_spec.rs @@ -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(), @@ -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 `` 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(), @@ -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", @@ -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(), @@ -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 `` list when it would help with the user's current request. Briefly explain why in `suggest_reason`.".to_string(); + + assert_eq!(recommendations, expected); + } } diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index d7f622c03..7cc72395f 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -40,11 +40,24 @@ pub struct ToolRouter { pub(crate) struct ToolRouterParams<'a> { pub(crate) mcp_tools: Option>, pub(crate) deferred_mcp_tools: Option>, - pub(crate) discoverable_tools: Option>, + pub(crate) tool_suggest_candidates: Option, pub(crate) extension_tool_executors: Vec>>, 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 { + pub(crate) tools: Vec, + pub(crate) presentation: ToolSuggestPresentation, +} + impl ToolRouter { pub(crate) fn from_turn_context( turn_context: &TurnContext, diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index 0a7345a84..a38ab8af2 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -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(), }, @@ -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( @@ -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(), }, @@ -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(), }, @@ -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, }, @@ -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(), }, diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 026d579a3..9269aa379 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -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; @@ -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>], dynamic_tools: &'a [DynamicToolSpec], tool_search_handler_cache: &'a ToolSearchHandlerCache, @@ -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; @@ -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, @@ -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, )); } diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 5faee2868..ee7cd7e35 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -36,12 +36,14 @@ use crate::tools::handlers::ToolSearchHandlerCache; use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE; use crate::tools::router::ToolRouter; use crate::tools::router::ToolRouterParams; +use crate::tools::router::ToolSuggestCandidates; +use crate::tools::router::ToolSuggestPresentation; #[derive(Default)] struct ToolPlanInputs { mcp_tools: Option>, deferred_mcp_tools: Option>, - discoverable_tools: Option>, + tool_suggest_candidates: Option, extension_tool_executors: Vec>>, dynamic_tools: Vec, } @@ -179,9 +181,9 @@ async fn probe_with( let router = ToolRouter::from_turn_context( &turn, ToolRouterParams { + tool_suggest_candidates: inputs.tool_suggest_candidates, mcp_tools: inputs.mcp_tools, deferred_mcp_tools: inputs.deferred_mcp_tools, - discoverable_tools: inputs.discoverable_tools, extension_tool_executors: inputs.extension_tool_executors, dynamic_tools: inputs.dynamic_tools.as_slice(), }, @@ -410,17 +412,19 @@ fn dynamic_tool(namespace: Option<&str>, name: &str, defer_loading: bool) -> Dyn } } -fn discoverable_plugin(id: &str, name: &str) -> DiscoverableTool { - DiscoverablePluginInfo { - id: id.to_string(), - remote_plugin_id: None, - name: name.to_string(), - description: Some(format!("{name} plugin")), - has_skills: false, - mcp_server_names: Vec::new(), - app_connector_ids: Vec::new(), +fn plugin_candidates(presentation: ToolSuggestPresentation) -> ToolSuggestCandidates { + ToolSuggestCandidates { + tools: vec![DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { + id: "github@openai-curated-remote".to_string(), + remote_plugin_id: None, + name: "GitHub".to_string(), + description: Some("Work with GitHub repositories".to_string()), + has_skills: true, + mcp_server_names: Vec::new(), + app_connector_ids: Vec::new(), + }))], + presentation, } - .into() } fn has_parameter(spec: &ToolSpec, parameter_name: &str) -> bool { @@ -793,7 +797,7 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() { ToolRouterParams { mcp_tools: None, deferred_mcp_tools: Some(vec![mcp_tool("first", "mcp__first", "lookup")]), - discoverable_tools: None, + tool_suggest_candidates: None, extension_tool_executors: Vec::new(), dynamic_tools: &[], }, @@ -808,7 +812,7 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() { ToolRouterParams { mcp_tools: None, deferred_mcp_tools: Some(vec![mcp_tool("second", "mcp__second", "lookup")]), - discoverable_tools: None, + tool_suggest_candidates: None, extension_tool_executors: Vec::new(), dynamic_tools: &[], }, @@ -853,8 +857,7 @@ async fn invalid_mcp_tools_are_not_registered() { } #[tokio::test] -async fn request_plugin_install_requires_all_discovery_features_and_discoverable_tools() { - let discoverable_tools = Some(vec![discoverable_plugin("github", "GitHub")]); +async fn request_plugin_install_requires_all_discovery_features() { for disabled_feature in [Feature::ToolSuggest, Feature::Apps, Feature::Plugins] { let plan = probe_with( |turn| { @@ -865,7 +868,7 @@ async fn request_plugin_install_requires_all_discovery_features_and_discoverable set_feature(turn, disabled_feature, /*enabled*/ false); }, ToolPlanInputs { - discoverable_tools: discoverable_tools.clone(), + tool_suggest_candidates: Some(plugin_candidates(ToolSuggestPresentation::ListTool)), ..ToolPlanInputs::default() }, ) @@ -876,17 +879,31 @@ async fn request_plugin_install_requires_all_discovery_features_and_discoverable ]); } - let no_candidates = probe(|turn| { - set_features( - turn, - &[Feature::ToolSuggest, Feature::Apps, Feature::Plugins], - ); - }) - .await; - no_candidates.assert_visible_lacks(&[ - "list_available_plugins_to_install", - "request_plugin_install", - ]); + for tool_suggest_candidates in [ + None, + Some(ToolSuggestCandidates { + tools: Vec::new(), + presentation: ToolSuggestPresentation::DeveloperContext, + }), + ] { + let plan = probe_with( + |turn| { + set_features( + turn, + &[Feature::ToolSuggest, Feature::Apps, Feature::Plugins], + ); + }, + ToolPlanInputs { + tool_suggest_candidates, + ..ToolPlanInputs::default() + }, + ) + .await; + plan.assert_visible_lacks(&[ + "list_available_plugins_to_install", + "request_plugin_install", + ]); + } let enabled = probe_with( |turn| { @@ -896,7 +913,7 @@ async fn request_plugin_install_requires_all_discovery_features_and_discoverable ); }, ToolPlanInputs { - discoverable_tools, + tool_suggest_candidates: Some(plugin_candidates(ToolSuggestPresentation::ListTool)), ..ToolPlanInputs::default() }, ) @@ -908,7 +925,7 @@ async fn request_plugin_install_requires_all_discovery_features_and_discoverable } #[tokio::test] -async fn install_suggestion_tools_stay_visible_without_tool_search() { +async fn request_plugin_install_stays_visible_without_tool_search() { let plan = probe_with( |turn| { turn.model_info.supports_search_tool = false; @@ -918,7 +935,7 @@ async fn install_suggestion_tools_stay_visible_without_tool_search() { ); }, ToolPlanInputs { - discoverable_tools: Some(vec![discoverable_plugin("github", "GitHub")]), + tool_suggest_candidates: Some(plugin_candidates(ToolSuggestPresentation::ListTool)), ..ToolPlanInputs::default() }, ) @@ -932,7 +949,7 @@ async fn install_suggestion_tools_stay_visible_without_tool_search() { } #[tokio::test] -async fn request_plugin_install_description_defers_inventory_to_list_tool() { +async fn request_plugin_install_description_refers_to_recommended_plugins_hint() { let plan = probe_with( |turn| { set_features( @@ -941,23 +958,14 @@ async fn request_plugin_install_description_defers_inventory_to_list_tool() { ); }, ToolPlanInputs { - discoverable_tools: Some(vec![discoverable_plugin("github", "GitHub")]), + tool_suggest_candidates: Some(plugin_candidates( + ToolSuggestPresentation::DeveloperContext, + )), ..ToolPlanInputs::default() }, ) .await; - let ToolSpec::Function(ResponsesApiTool { - description: list_description, - .. - }) = plan.visible_spec("list_available_plugins_to_install") - else { - panic!("expected list_available_plugins_to_install function spec"); - }; - assert!(list_description.contains( - "Returns known plugins and connectors that can be passed to `request_plugin_install`." - )); - let ToolSpec::Function(ResponsesApiTool { description: request_description, .. @@ -965,10 +973,11 @@ async fn request_plugin_install_description_defers_inventory_to_list_tool() { else { panic!("expected request_plugin_install function spec"); }; - assert!(request_description.contains( - "Use this tool only after `list_available_plugins_to_install` returns a plugin or connector that exactly matches the user's explicit request." - )); + assert!(request_description.contains("developer `` list")); + assert!(!request_description.contains("list_available_plugins_to_install")); assert!(!request_description.contains("github")); + plan.assert_visible_lacks(&["list_available_plugins_to_install"]); + plan.assert_registered_lacks(&["list_available_plugins_to_install"]); } #[tokio::test]