mod apply_code_action_tool;mod context_server_registry;mod copy_path_tool;mod create_directory_tool;mod create_thread_tool;mod delete_path_tool;mod diagnostics_tool;mod edit_file_tool;mod edit_session;#[cfg(all(test, feature = "unit-eval"))]mod evals;mod fetch_tool;mod find_path_tool;mod find_references_tool;mod get_code_actions_tool;mod go_to_definition_tool;mod grep_tool;mod list_agents_and_models_tool;mod list_directory_tool;mod move_path_tool;mod read_file_tool;mod rename_tool;mod skill_tool;mod spawn_agent_tool;mod symbol_locator;mod terminal_tool;mod tool_permissions;mod web_search_tool;mod write_file_tool; use crate::AgentTool;use feature_flags::{ CreateThreadToolFeatureFlag, FeatureFlagAppExt as _, LspToolFeatureFlag, RenameToolFeatureFlag,};use gpui::App;use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat};use serde::{ Deserialize, Deserializer, de::{DeserializeOwned, Error as _},}; /// Deserialize a value that may have been provided as a JSON-encoded string/// instead of the structured value. Some models occasionally stringify nested/// arguments, so we accept either form.pub(crate) fn deserialize_maybe_stringified<'de, T, D>(deserializer: D) -> Result<T, D::Error>where T: DeserializeOwned, D: Deserializer<'de>,{ fn to_custom_error<E>(e: serde_json::Error) -> E where E: serde::de::Error, { E::custom(format!("{e}")) } let raw_value = serde_json::Value::deserialize(deserializer) .map_err(|error| D::Error::custom(format!("invalid JSON: {error}")))?; match T::deserialize(&raw_value) { Ok(value) => Ok(value), Err(original_error) => { let Some(string) = raw_value.as_str() else { return Err(to_custom_error(original_error)); }; serde_json::from_str(string).map_err(to_custom_error) } }} pub use apply_code_action_tool::*;pub use context_server_registry::*;pub use copy_path_tool::*;pub use create_directory_tool::*;pub use create_thread_tool::*;pub use delete_path_tool::*;pub use diagnostics_tool::*;pub use edit_file_tool::*;pub use fetch_tool::*;pub use find_path_tool::*;pub use find_references_tool::*;pub use get_code_actions_tool::*;pub use go_to_definition_tool::*;pub use grep_tool::*;pub use list_agents_and_models_tool::*;pub use list_directory_tool::*;pub use move_path_tool::*;pub use read_file_tool::*;pub use rename_tool::*;pub use skill_tool::*;pub use spawn_agent_tool::*;pub use symbol_locator::*; pub use terminal_tool::*;pub use tool_permissions::*;pub use web_search_tool::*;pub use write_file_tool::*; macro_rules! tools { ($($tool:ty),* $(,)?) => { /// Every built-in tool name, determined at compile time. pub const ALL_TOOL_NAMES: &[&str] = &[ $(<$tool>::NAME,)* ]; const _: () = { const fn str_eq(a: &str, b: &str) -> bool { let a = a.as_bytes(); let b = b.as_bytes(); if a.len() != b.len() { return false; } let mut i = 0; while i < a.len() { if a[i] != b[i] { return false; } i += 1; } true } const NAMES: &[&str] = ALL_TOOL_NAMES; let mut i = 0; while i < NAMES.len() { let mut j = i + 1; while j < NAMES.len() { if str_eq(NAMES[i], NAMES[j]) { panic!("Duplicate tool name in tools! macro"); } j += 1; } i += 1; } }; /// Returns whether the tool with the given name supports the given provider. pub fn tool_supports_provider(name: &str, provider: &language_model::LanguageModelProviderId) -> bool { $( if name == <$tool>::NAME { return <$tool>::supports_provider(provider); } )* false } /// Returns whether the tool with the given name may be provided to an /// agent in a restricted workspace. Unknown tools (e.g. MCP tools) are /// considered allowed. pub fn tool_allowed_in_restricted_mode(name: &str) -> bool { $( if name == <$tool>::NAME { return <$tool>::allow_in_restricted_mode(); } )* true } /// A list of all built-in tools pub fn built_in_tools() -> impl Iterator<Item = LanguageModelRequestTool> { fn language_model_tool<T: AgentTool>() -> LanguageModelRequestTool { LanguageModelRequestTool::function( T::NAME.to_string(), T::description().to_string(), T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(), T::supports_input_streaming(), ) } [ $( language_model_tool::<$tool>(), )* ] .into_iter() } };} // Adding a tool here (and constructing it in `Thread::add_default_tools`) is// not enough to make the model actually receive it. Three further gates will// silently drop the tool rather than fail to compile://// 1. `assets/settings/default.json`: the `write` and `ask` agent profiles each// carry an explicit `tools` allowlist. `Thread::enabled_tools` filters out// any tool not present there with value `true`, so it never reaches the// model.// 2. `test_all_tools_are_in_tool_info_or_excluded` in// `crates/settings_ui/src/pages/tool_permissions_setup.rs`: every tool must// be in the permission-UI `TOOLS` list (if it calls// `decide_permission_from_settings`) or in `EXCLUDED_TOOLS`.// 3. `tool_feature_flag_enabled`: some tools are gated behind a feature flag and// are dropped unless it is active. The agent-profile UI uses the same gate so// it never offers a tool the agent can't actually use.tools! { ApplyCodeActionTool, CopyPathTool, CreateDirectoryTool, CreateThreadTool, DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, FindReferencesTool, GetCodeActionsTool, GoToDefinitionTool, GrepTool, ListAgentsAndModelsTool, ListDirectoryTool, MovePathTool, ReadFileTool, RenameTool, SkillTool, SpawnAgentTool, TerminalTool, WebSearchTool, WriteFileTool,} /// Some built-in tools are gated behind a feature flag and only become usable/// once that flag is active. Tools without a flag are always available.////// This is the single source of truth for that gating: `Thread::enabled_tools`/// uses it to decide what the model receives, and the agent-profile/// configuration UI uses it to decide what to offer — so the UI can never list/// a tool the agent would silently drop (see #56778).pub fn tool_feature_flag_enabled(tool_name: &str, cx: &App) -> bool { match tool_name { RenameTool::NAME => cx.has_flag::<RenameToolFeatureFlag>(), FindReferencesTool::NAME | GetCodeActionsTool::NAME | ApplyCodeActionTool::NAME | GoToDefinitionTool::NAME => cx.has_flag::<LspToolFeatureFlag>(), CreateThreadTool::NAME | ListAgentsAndModelsTool::NAME => { cx.has_flag::<CreateThreadToolFeatureFlag>() } _ => true, }} #[cfg(test)]mod tests { use super::*; #[test] fn fetch_and_terminal_are_forbidden_in_restricted_mode() { assert!(!tool_allowed_in_restricted_mode(FetchTool::NAME)); assert!(!tool_allowed_in_restricted_mode(TerminalTool::NAME)); // Every other built-in tool, and unknown (e.g. MCP) tools, are allowed. for name in ALL_TOOL_NAMES { let expected = *name != FetchTool::NAME && *name != TerminalTool::NAME; assert_eq!( tool_allowed_in_restricted_mode(name), expected, "unexpected restricted-mode policy for tool `{name}`" ); } assert!(tool_allowed_in_restricted_mode("some_mcp_tool")); }}
mod apply_code_action_tool;
mod context_server_registry;
mod copy_path_tool;
mod create_directory_tool;
mod create_thread_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod edit_session;
#[cfg(all(test, feature = "unit-eval"))]
mod evals;
mod fetch_tool;
mod find_path_tool;
mod find_references_tool;
mod get_code_actions_tool;
mod go_to_definition_tool;
mod grep_tool;
mod list_agents_and_models_tool;
mod list_directory_tool;
mod move_path_tool;
mod read_file_tool;
mod rename_tool;
mod skill_tool;
mod spawn_agent_tool;
mod symbol_locator;
mod terminal_tool;
mod tool_permissions;
mod web_search_tool;
mod write_file_tool;
use crate::AgentTool;
use feature_flags::{
CreateThreadToolFeatureFlag, FeatureFlagAppExt as _, LspToolFeatureFlag, RenameToolFeatureFlag,
};
use gpui::App;
use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat};
use serde::{
Deserialize, Deserializer,
de::{DeserializeOwned, Error as _},
};
/// Deserialize a value that may have been provided as a JSON-encoded string
/// instead of the structured value. Some models occasionally stringify nested
/// arguments, so we accept either form.
pub(crate) fn deserialize_maybe_stringified<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: DeserializeOwned,
D: Deserializer<'de>,
{
fn to_custom_error<E>(e: serde_json::Error) -> E
where
E: serde::de::Error,
{
E::custom(format!("{e}"))
}
let raw_value = serde_json::Value::deserialize(deserializer)
.map_err(|error| D::Error::custom(format!("invalid JSON: {error}")))?;
match T::deserialize(&raw_value) {
Ok(value) => Ok(value),
Err(original_error) => {
let Some(string) = raw_value.as_str() else {
return Err(to_custom_error(original_error));
};
serde_json::from_str(string).map_err(to_custom_error)
}
}
}
pub use apply_code_action_tool::*;
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
pub use create_thread_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*;
pub use find_references_tool::*;
pub use get_code_actions_tool::*;
pub use go_to_definition_tool::*;
pub use grep_tool::*;
pub use list_agents_and_models_tool::*;
pub use list_directory_tool::*;
pub use move_path_tool::*;
pub use read_file_tool::*;
pub use rename_tool::*;
pub use skill_tool::*;
pub use spawn_agent_tool::*;
pub use symbol_locator::*;
pub use terminal_tool::*;
pub use tool_permissions::*;
pub use web_search_tool::*;
pub use write_file_tool::*;
macro_rules! tools {
($($tool:ty),* $(,)?) => {
/// Every built-in tool name, determined at compile time.
pub const ALL_TOOL_NAMES: &[&str] = &[
$(<$tool>::NAME,)*
];
const _: () = {
const fn str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const NAMES: &[&str] = ALL_TOOL_NAMES;
let mut i = 0;
while i < NAMES.len() {
let mut j = i + 1;
while j < NAMES.len() {
if str_eq(NAMES[i], NAMES[j]) {
panic!("Duplicate tool name in tools! macro");
}
j += 1;
}
i += 1;
}
};
/// Returns whether the tool with the given name supports the given provider.
pub fn tool_supports_provider(name: &str, provider: &language_model::LanguageModelProviderId) -> bool {
$(
if name == <$tool>::NAME {
return <$tool>::supports_provider(provider);
}
)*
false
}
/// Returns whether the tool with the given name may be provided to an
/// agent in a restricted workspace. Unknown tools (e.g. MCP tools) are
/// considered allowed.
pub fn tool_allowed_in_restricted_mode(name: &str) -> bool {
$(
if name == <$tool>::NAME {
return <$tool>::allow_in_restricted_mode();
}
)*
true
}
/// A list of all built-in tools
pub fn built_in_tools() -> impl Iterator<Item = LanguageModelRequestTool> {
fn language_model_tool<T: AgentTool>() -> LanguageModelRequestTool {
LanguageModelRequestTool::function(
T::NAME.to_string(),
T::description().to_string(),
T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(),
T::supports_input_streaming(),
)
}
[
$(
language_model_tool::<$tool>(),
)*
]
.into_iter()
}
};
}
// Adding a tool here (and constructing it in `Thread::add_default_tools`) is
// not enough to make the model actually receive it. Three further gates will
// silently drop the tool rather than fail to compile:
//
// 1. `assets/settings/default.json`: the `write` and `ask` agent profiles each
// carry an explicit `tools` allowlist. `Thread::enabled_tools` filters out
// any tool not present there with value `true`, so it never reaches the
// model.
// 2. `test_all_tools_are_in_tool_info_or_excluded` in
// `crates/settings_ui/src/pages/tool_permissions_setup.rs`: every tool must
// be in the permission-UI `TOOLS` list (if it calls
// `decide_permission_from_settings`) or in `EXCLUDED_TOOLS`.
// 3. `tool_feature_flag_enabled`: some tools are gated behind a feature flag and
// are dropped unless it is active. The agent-profile UI uses the same gate so
// it never offers a tool the agent can't actually use.
tools! {
ApplyCodeActionTool,
CopyPathTool,
CreateDirectoryTool,
CreateThreadTool,
DeletePathTool,
DiagnosticsTool,
EditFileTool,
FetchTool,
FindPathTool,
FindReferencesTool,
GetCodeActionsTool,
GoToDefinitionTool,
GrepTool,
ListAgentsAndModelsTool,
ListDirectoryTool,
MovePathTool,
ReadFileTool,
RenameTool,
SkillTool,
SpawnAgentTool,
TerminalTool,
WebSearchTool,
WriteFileTool,
}
/// Some built-in tools are gated behind a feature flag and only become usable
/// once that flag is active. Tools without a flag are always available.
///
/// This is the single source of truth for that gating: `Thread::enabled_tools`
/// uses it to decide what the model receives, and the agent-profile
/// configuration UI uses it to decide what to offer — so the UI can never list
/// a tool the agent would silently drop (see #56778).
pub fn tool_feature_flag_enabled(tool_name: &str, cx: &App) -> bool {
match tool_name {
RenameTool::NAME => cx.has_flag::<RenameToolFeatureFlag>(),
FindReferencesTool::NAME
| GetCodeActionsTool::NAME
| ApplyCodeActionTool::NAME
| GoToDefinitionTool::NAME => cx.has_flag::<LspToolFeatureFlag>(),
CreateThreadTool::NAME | ListAgentsAndModelsTool::NAME => {
cx.has_flag::<CreateThreadToolFeatureFlag>()
}
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fetch_and_terminal_are_forbidden_in_restricted_mode() {
assert!(!tool_allowed_in_restricted_mode(FetchTool::NAME));
assert!(!tool_allowed_in_restricted_mode(TerminalTool::NAME));
// Every other built-in tool, and unknown (e.g. MCP) tools, are allowed.
for name in ALL_TOOL_NAMES {
let expected = *name != FetchTool::NAME && *name != TerminalTool::NAME;
assert_eq!(
tool_allowed_in_restricted_mode(name),
expected,
"unexpected restricted-mode policy for tool `{name}`"
);
}
assert!(tool_allowed_in_restricted_mode("some_mcp_tool"));
}
}