agent: Show a warning when some tools are incompatible with the selected model (#28755)

Bennet Bo Fenner and Danilo Leal created

WIP

<img width="644" alt="image"
src="https://github.com/user-attachments/assets/b24e1a57-f82e-457c-b788-1b314ade7c84"
/>


<img width="644" alt="image"
src="https://github.com/user-attachments/assets/b158953c-2015-4cc8-b8ed-35c6fcbe162d"
/>


Release Notes:

- agent: Improve compatibility with Gemini Tool Calling APIs. When a
tool is incompatible with the Gemini APIs a warning indicator will be
displayed. Incompatible tools will be automatically excluded from the
conversation

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/agent/src/assistant.rs               |   1 
crates/agent/src/message_editor.rs          | 224 ++++++++++++++--------
crates/agent/src/tool_compatibility.rs      |  89 +++++++++
crates/language_model/src/language_model.rs |   2 
4 files changed, 229 insertions(+), 87 deletions(-)

Detailed changes

crates/agent/src/assistant.rs 🔗

@@ -18,6 +18,7 @@ mod terminal_inline_assistant;
 mod thread;
 mod thread_history;
 mod thread_store;
+mod tool_compatibility;
 mod tool_use;
 mod ui;
 

crates/agent/src/message_editor.rs 🔗

@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
 use std::sync::Arc;
 
 use crate::assistant_model_selector::ModelType;
+use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
 use buffer_diff::BufferDiff;
 use collections::HashSet;
 use editor::actions::MoveUp;
@@ -41,6 +42,7 @@ use crate::{
 
 pub struct MessageEditor {
     thread: Entity<Thread>,
+    incompatible_tools_state: Entity<IncompatibleToolsState>,
     editor: Entity<Editor>,
     #[allow(dead_code)]
     workspace: WeakEntity<Workspace>,
@@ -124,6 +126,9 @@ impl MessageEditor {
             )
         });
 
+        let incompatible_tools =
+            cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
+
         let subscriptions =
             vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)];
 
@@ -131,6 +136,7 @@ impl MessageEditor {
             editor: editor.clone(),
             project: thread.read(cx).project().clone(),
             thread,
+            incompatible_tools_state: incompatible_tools.clone(),
             workspace,
             context_store,
             context_strip,
@@ -368,6 +374,23 @@ impl MessageEditor {
         let is_model_selected = self.is_model_selected(cx);
         let is_editor_empty = self.is_editor_empty(cx);
 
+        let model = LanguageModelRegistry::read_global(cx)
+            .default_model()
+            .map(|default| default.model.clone());
+
+        let incompatible_tools = model
+            .as_ref()
+            .map(|model| {
+                self.incompatible_tools_state.update(cx, |state, cx| {
+                    state
+                        .incompatible_tools(model, cx)
+                        .iter()
+                        .cloned()
+                        .collect::<Vec<_>>()
+                })
+            })
+            .unwrap_or_default();
+
         let is_editor_expanded = self.editor_is_expanded;
         let expand_icon = if is_editor_expanded {
             IconName::Minimize
@@ -472,54 +495,80 @@ impl MessageEditor {
                             .flex_none()
                             .justify_between()
                             .child(h_flex().gap_2().child(self.profile_selector.clone()))
-                            .child(h_flex().gap_1().child(self.model_selector.clone()).map({
-                                let focus_handle = focus_handle.clone();
-                                move |parent| {
-                                    if is_generating {
-                                        parent
-                                            .when(is_editor_empty, |parent| {
-                                                parent.child(
-                                                    IconButton::new(
-                                                        "stop-generation",
-                                                        IconName::StopFilled,
-                                                    )
-                                                    .icon_color(Color::Error)
-                                                    .style(ButtonStyle::Tinted(
-                                                        ui::TintColor::Error,
-                                                    ))
-                                                    .tooltip(move |window, cx| {
-                                                        Tooltip::for_action(
-                                                            "Stop Generation",
-                                                            &editor::actions::Cancel,
-                                                            window,
-                                                            cx,
-                                                        )
+                            .child(
+                                h_flex()
+                                    .gap_1()
+                                    .when(!incompatible_tools.is_empty(), |this| {
+                                        this.child(
+                                            IconButton::new(
+                                                "tools-incompatible-warning",
+                                                IconName::Warning,
+                                            )
+                                            .icon_color(Color::Warning)
+                                            .icon_size(IconSize::Small)
+                                            .tooltip({
+                                                move |_, cx| {
+                                                    cx.new(|_| IncompatibleToolsTooltip {
+                                                        incompatible_tools: incompatible_tools
+                                                            .clone(),
                                                     })
-                                                    .on_click({
-                                                        let focus_handle = focus_handle.clone();
-                                                        move |_event, window, cx| {
-                                                            focus_handle.dispatch_action(
-                                                                &editor::actions::Cancel,
-                                                                window,
-                                                                cx,
-                                                            );
-                                                        }
+                                                    .into()
+                                                }
+                                            }),
+                                        )
+                                    })
+                                    .child(self.model_selector.clone())
+                                    .map({
+                                        let focus_handle = focus_handle.clone();
+                                        move |parent| {
+                                            if is_generating {
+                                                parent
+                                                    .when(is_editor_empty, |parent| {
+                                                        parent.child(
+                                                            IconButton::new(
+                                                                "stop-generation",
+                                                                IconName::StopFilled,
+                                                            )
+                                                            .icon_color(Color::Error)
+                                                            .style(ButtonStyle::Tinted(
+                                                                ui::TintColor::Error,
+                                                            ))
+                                                            .tooltip(move |window, cx| {
+                                                                Tooltip::for_action(
+                                                                    "Stop Generation",
+                                                                    &editor::actions::Cancel,
+                                                                    window,
+                                                                    cx,
+                                                                )
+                                                            })
+                                                            .on_click({
+                                                                let focus_handle =
+                                                                    focus_handle.clone();
+                                                                move |_event, window, cx| {
+                                                                    focus_handle.dispatch_action(
+                                                                        &editor::actions::Cancel,
+                                                                        window,
+                                                                        cx,
+                                                                    );
+                                                                }
+                                                            })
+                                                            .with_animation(
+                                                                "pulsating-label",
+                                                                Animation::new(
+                                                                    Duration::from_secs(2),
+                                                                )
+                                                                .repeat()
+                                                                .with_easing(pulsating_between(
+                                                                    0.4, 1.0,
+                                                                )),
+                                                                |icon_button, delta| {
+                                                                    icon_button.alpha(delta)
+                                                                },
+                                                            ),
+                                                        )
                                                     })
-                                                    .with_animation(
-                                                        "pulsating-label",
-                                                        Animation::new(Duration::from_secs(2))
-                                                            .repeat()
-                                                            .with_easing(pulsating_between(
-                                                                0.4, 1.0,
-                                                            )),
-                                                        |icon_button, delta| {
-                                                            icon_button.alpha(delta)
-                                                        },
-                                                    ),
-                                                )
-                                            })
-                                            .when(!is_editor_empty, |parent| {
-                                                parent.child(
+                                                    .when(!is_editor_empty, |parent| {
+                                                        parent.child(
                                                     IconButton::new("send-message", IconName::Send)
                                                         .icon_color(Color::Accent)
                                                         .style(ButtonStyle::Filled)
@@ -545,48 +594,51 @@ impl MessageEditor {
                                                             )
                                                         }),
                                                 )
-                                            })
-                                    } else {
-                                        parent.child(
-                                            IconButton::new("send-message", IconName::Send)
-                                                .icon_color(Color::Accent)
-                                                .style(ButtonStyle::Filled)
-                                                .disabled(
-                                                    is_editor_empty
-                                                        || !is_model_selected
-                                                        || self.waiting_for_summaries_to_send,
-                                                )
-                                                .on_click({
-                                                    let focus_handle = focus_handle.clone();
-                                                    move |_event, window, cx| {
-                                                        focus_handle
-                                                            .dispatch_action(&Chat, window, cx);
-                                                    }
-                                                })
-                                                .when(
-                                                    !is_editor_empty && is_model_selected,
-                                                    |button| {
-                                                        button.tooltip(move |window, cx| {
-                                                            Tooltip::for_action(
-                                                                "Send", &Chat, window, cx,
-                                                            )
+                                                    })
+                                            } else {
+                                                parent.child(
+                                                    IconButton::new("send-message", IconName::Send)
+                                                        .icon_color(Color::Accent)
+                                                        .style(ButtonStyle::Filled)
+                                                        .disabled(
+                                                            is_editor_empty
+                                                                || !is_model_selected
+                                                                || self
+                                                                    .waiting_for_summaries_to_send,
+                                                        )
+                                                        .on_click({
+                                                            let focus_handle = focus_handle.clone();
+                                                            move |_event, window, cx| {
+                                                                focus_handle.dispatch_action(
+                                                                    &Chat, window, cx,
+                                                                );
+                                                            }
                                                         })
-                                                    },
+                                                        .when(
+                                                            !is_editor_empty && is_model_selected,
+                                                            |button| {
+                                                                button.tooltip(move |window, cx| {
+                                                                    Tooltip::for_action(
+                                                                        "Send", &Chat, window, cx,
+                                                                    )
+                                                                })
+                                                            },
+                                                        )
+                                                        .when(is_editor_empty, |button| {
+                                                            button.tooltip(Tooltip::text(
+                                                                "Type a message to submit",
+                                                            ))
+                                                        })
+                                                        .when(!is_model_selected, |button| {
+                                                            button.tooltip(Tooltip::text(
+                                                                "Select a model to continue",
+                                                            ))
+                                                        }),
                                                 )
-                                                .when(is_editor_empty, |button| {
-                                                    button.tooltip(Tooltip::text(
-                                                        "Type a message to submit",
-                                                    ))
-                                                })
-                                                .when(!is_model_selected, |button| {
-                                                    button.tooltip(Tooltip::text(
-                                                        "Select a model to continue",
-                                                    ))
-                                                }),
-                                        )
-                                    }
-                                }
-                            })),
+                                            }
+                                        }
+                                    }),
+                            ),
                     ),
             )
     }

crates/agent/src/tool_compatibility.rs 🔗

@@ -0,0 +1,89 @@
+use std::sync::Arc;
+
+use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent};
+use collections::HashMap;
+use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
+use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
+use ui::prelude::*;
+
+pub struct IncompatibleToolsState {
+    cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
+    tool_working_set: Entity<ToolWorkingSet>,
+    _tool_working_set_subscription: Subscription,
+}
+
+impl IncompatibleToolsState {
+    pub fn new(tool_working_set: Entity<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
+        let _tool_working_set_subscription =
+            cx.subscribe(&tool_working_set, |this, _, event, _| match event {
+                ToolWorkingSetEvent::EnabledToolsChanged => {
+                    this.cache.clear();
+                }
+            });
+
+        Self {
+            cache: HashMap::default(),
+            tool_working_set,
+            _tool_working_set_subscription,
+        }
+    }
+
+    pub fn incompatible_tools(
+        &mut self,
+        model: &Arc<dyn LanguageModel>,
+        cx: &App,
+    ) -> &[Arc<dyn Tool>] {
+        self.cache
+            .entry(model.tool_input_format())
+            .or_insert_with(|| {
+                self.tool_working_set
+                    .read(cx)
+                    .enabled_tools(cx)
+                    .iter()
+                    .filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
+                    .cloned()
+                    .collect()
+            })
+    }
+}
+
+pub struct IncompatibleToolsTooltip {
+    pub incompatible_tools: Vec<Arc<dyn Tool>>,
+}
+
+impl Render for IncompatibleToolsTooltip {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        ui::tooltip_container(window, cx, |container, _, cx| {
+            container
+                .w_72()
+                .child(Label::new("Incompatible Tools").size(LabelSize::Small))
+                .child(
+                    Label::new(
+                        "This model is incompatible with the following tools from your MCPs:",
+                    )
+                    .size(LabelSize::Small)
+                    .color(Color::Muted),
+                )
+                .child(
+                    v_flex()
+                        .my_1p5()
+                        .py_0p5()
+                        .border_b_1()
+                        .border_color(cx.theme().colors().border_variant)
+                        .children(
+                            self.incompatible_tools
+                                .iter()
+                                .map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)),
+                        ),
+                )
+                .child(Label::new("What To Do Instead").size(LabelSize::Small))
+                .child(
+                    Label::new(
+                        "Every other tool continues to work with this model, but to specifically use those, switch to another model.",
+                    )
+                    .size(LabelSize::Small)
+                    .color(Color::Muted),
+                )
+        })
+    }
+}

crates/language_model/src/language_model.rs 🔗

@@ -67,7 +67,7 @@ pub enum LanguageModelCompletionEvent {
 }
 
 /// Indicates the format used to define the input schema for a language model tool.
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
 pub enum LanguageModelToolSchemaFormat {
     /// A JSON schema, see https://json-schema.org
     JsonSchema,