From cbe85e117b1db59cdbe8175c59793c3cf2a4a7b8 Mon Sep 17 00:00:00 2001 From: dozaki-openai Date: Fri, 14 Aug 2026 02:50:04 +0000 Subject: [PATCH] Search selected plugin apps before falling back (#38484) ## What changed - When an explicitly selected plugin has apps available, instruct the model to use `tool_search` to discover relevant tools before falling back to unrelated or built-in tools. - Scope the guidance to the request that selected the plugin and omit it for plugins without available apps. ## Testing - Cover app-specific rendering, non-app plugins, bounded instruction output, and app enablement in dual-surface plugin requests. GitOrigin-RevId: 1b9501cc403d2817334a9307e683e99c140a794a --- codex-rs/core/src/plugins/render.rs | 33 ++++++++++++++------- codex-rs/core/src/plugins/render_tests.rs | 35 ++++++++++++++++++++++- codex-rs/core/tests/suite/plugins.rs | 7 +++++ 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/plugins/render.rs b/codex-rs/core/src/plugins/render.rs index 38dc949b2f..d6f747a4c9 100644 --- a/codex-rs/core/src/plugins/render.rs +++ b/codex-rs/core/src/plugins/render.rs @@ -24,6 +24,17 @@ pub(crate) fn render_explicit_plugin_instructions( plugin.display_name )]; + if !available_apps.is_empty() { + lines.push( + concat!( + "- For the user request that explicitly selected this plugin, and only for that ", + "request, if `tool_search` is available and an app from this plugin may help, ", + "search for its tools before falling back to unrelated or built-in tools." + ) + .to_string(), + ); + } + if plugin.has_skills { let skill_namespace = plugin .plugin_namespace @@ -34,17 +45,6 @@ pub(crate) fn render_explicit_plugin_instructions( )); } - if !available_mcp_servers.is_empty() { - lines.push(format!( - "- MCP servers from this plugin available in this session: {}.", - available_mcp_servers - .iter() - .map(|server| format!("`{server}`")) - .collect::>() - .join(", ") - )); - } - if !available_apps.is_empty() { lines.push(format!( "- Apps from this plugin available in this session: {}.", @@ -56,6 +56,17 @@ pub(crate) fn render_explicit_plugin_instructions( )); } + if !available_mcp_servers.is_empty() { + lines.push(format!( + "- MCP servers from this plugin available in this session: {}.", + available_mcp_servers + .iter() + .map(|server| format!("`{server}`")) + .collect::>() + .join(", ") + )); + } + if lines.len() == 1 { return None; } diff --git a/codex-rs/core/src/plugins/render_tests.rs b/codex-rs/core/src/plugins/render_tests.rs index e264f8b2b1..8dca0e720c 100644 --- a/codex-rs/core/src/plugins/render_tests.rs +++ b/codex-rs/core/src/plugins/render_tests.rs @@ -40,6 +40,31 @@ fn explicit_plugin_instructions_use_manifest_namespace_for_skills() { assert!(rendered.contains("`acme.tools:`")); assert!(!rendered.contains("`Acme Developer Tools:`")); + assert!(!rendered.contains("tool_search")); +} + +#[test] +fn explicit_plugin_instructions_search_available_apps_before_fallback() { + let rendered = render_explicit_plugin_instructions( + &PluginCapabilitySummary { + config_name: "app-adobe@openai-curated-remote".to_string(), + display_name: "Adobe".to_string(), + ..PluginCapabilitySummary::default() + }, + &[], + &["Adobe".to_string()], + ) + .expect("app capability should render"); + + assert_eq!( + rendered, + "Capabilities from the `Adobe` plugin:\n\ + - For the user request that explicitly selected this plugin, and only for that request, \ + if `tool_search` is available and an app from this plugin may help, search for its \ + tools before falling back to unrelated or built-in tools.\n\ + - Apps from this plugin available in this session: `Adobe`.\n\ + Use these plugin-associated capabilities to help solve the task." + ); } #[test] @@ -47,18 +72,26 @@ fn explicit_plugin_instructions_are_bounded() { let servers = (0..1_024) .map(|index| format!("server-{index}")) .collect::>(); + let apps = (0..1_024) + .map(|index| format!("app-{index}")) + .collect::>(); let rendered = render_explicit_plugin_instructions( &PluginCapabilitySummary { config_name: "sample@test".to_string(), display_name: "sample".to_string(), + has_skills: true, ..PluginCapabilitySummary::default() }, &servers, - &[], + &apps, ) .expect("MCP capability should render"); assert!(rendered.len() <= MAX_EXPLICIT_PLUGIN_INSTRUCTIONS_BYTES); + assert!(rendered.contains("only for that request")); + assert!(rendered.contains("if `tool_search` is available")); + assert!(rendered.contains("Skills from this plugin")); + assert!(rendered.contains("`app-0`")); assert!(rendered.ends_with(TRUNCATED_PLUGIN_INSTRUCTIONS_SUFFIX)); } diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index a77c7f726b..c8041493a9 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -1134,6 +1134,13 @@ async fn explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins( app_enabled, "plugin app guidance should match app enablement: {developer_messages:?}" ); + assert_eq!( + developer_messages + .iter() + .any(|text| text.contains("if `tool_search` is available")), + app_enabled, + "plugin app search guidance should match app enablement: {developer_messages:?}" + ); assert!( request .tool_by_name(SAMPLE_PLUGIN_MCP_NAMESPACE, "echo")