Add dedicated read_skill tool

Nathan Sobo created

Introduce a standalone SkillTool (read_skill) rather than routing skill
reads through the existing read_file and list_directory tools. Add
read_skill to the default tool profiles.

Gate agent skills behind an AgentSkillsFeatureFlag.

Change summary

Cargo.toml                           |   1 
assets/settings/default.json         |   2 
crates/agent/Cargo.toml              |   1 
crates/agent/src/thread.rs           |  12 +
crates/agent/src/tools.rs            |   3 
crates/agent/src/tools/skill_tool.rs | 170 ++++++++++++++++++++++++++++++
crates/feature_flags/src/flags.rs    |  10 +
7 files changed, 195 insertions(+), 4 deletions(-)

Detailed changes

Cargo.toml 🔗

@@ -698,6 +698,7 @@ serde_json_lenient = { version = "0.2", features = [
 serde_path_to_error = "0.1.17"
 serde_urlencoded = "0.7"
 yaml_serde = "0.10"
+
 sha2 = "0.10"
 shellexpand = "2.1.0"
 shlex = "1.3.0"

assets/settings/default.json 🔗

@@ -1059,6 +1059,7 @@
           "now": true,
           "find_path": true,
           "read_file": true,
+          "read_skill": true,
           "restore_file_from_disk": true,
           "save_file": true,
           "open": true,
@@ -1082,6 +1083,7 @@
           "now": true,
           "find_path": true,
           "read_file": true,
+          "read_skill": true,
           "open": true,
           "grep": true,
           "spawn_agent": true,

crates/agent/Cargo.toml 🔗

@@ -58,6 +58,7 @@ schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 yaml_serde.workspace = true
+
 settings.workspace = true
 shell_command_parser.workspace = true
 smallvec.workspace = true

crates/agent/src/thread.rs 🔗

@@ -2,14 +2,15 @@ use crate::{
     ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
     DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
     ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool,
-    RestoreFileFromDiskTool, SaveFileTool, SkillsContext, SpawnAgentTool, StreamingEditFileTool,
-    SystemPromptTemplate, Template, Templates, TerminalTool, ToolPermissionDecision,
-    UpdatePlanTool, WebSearchTool, decide_permission_from_settings,
+    RestoreFileFromDiskTool, SaveFileTool, SkillTool, SkillsContext, SpawnAgentTool,
+    StreamingEditFileTool, SystemPromptTemplate, Template, Templates, TerminalTool,
+    ToolPermissionDecision, UpdatePlanTool, WebSearchTool, decide_permission_from_settings,
 };
 use acp_thread::{MentionUri, UserMessageId};
 use action_log::ActionLog;
 use feature_flags::{
-    FeatureFlagAppExt as _, StreamingEditFileToolFeatureFlag, UpdatePlanToolFeatureFlag,
+    AgentSkillsFeatureFlag, FeatureFlagAppExt as _, StreamingEditFileToolFeatureFlag,
+    UpdatePlanToolFeatureFlag,
 };
 
 use agent_client_protocol as acp;
@@ -1553,6 +1554,9 @@ impl Thread {
             update_agent_location,
         ));
         self.add_tool(SaveFileTool::new(self.project.clone()));
+        if cx.has_flag::<AgentSkillsFeatureFlag>() {
+            self.add_tool(SkillTool::new(self.project.clone()));
+        }
         self.add_tool(RestoreFileFromDiskTool::new(self.project.clone()));
         self.add_tool(TerminalTool::new(self.project.clone(), environment.clone()));
         self.add_tool(WebSearchTool);

crates/agent/src/tools.rs 🔗

@@ -16,6 +16,7 @@ mod open_tool;
 mod read_file_tool;
 mod restore_file_from_disk_tool;
 mod save_file_tool;
+mod skill_tool;
 mod spawn_agent_tool;
 mod streaming_edit_file_tool;
 mod terminal_tool;
@@ -43,6 +44,7 @@ pub use open_tool::*;
 pub use read_file_tool::*;
 pub use restore_file_from_disk_tool::*;
 pub use save_file_tool::*;
+pub use skill_tool::*;
 pub use spawn_agent_tool::*;
 pub use streaming_edit_file_tool::*;
 pub use terminal_tool::*;
@@ -134,6 +136,7 @@ tools! {
     ReadFileTool,
     RestoreFileFromDiskTool,
     SaveFileTool,
+    SkillTool,
     SpawnAgentTool,
     TerminalTool,
     UpdatePlanTool,

crates/agent/src/tools/skill_tool.rs 🔗

@@ -0,0 +1,170 @@
+use super::tool_permissions::canonicalize_worktree_roots;
+use crate::{AgentTool, ToolCallEventStream, ToolInput};
+use agent_client_protocol as acp;
+use futures::StreamExt;
+use gpui::{App, Entity, SharedString, Task};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::fmt::Write;
+use std::sync::Arc;
+
+/// Reads a file or lists a directory within an agent skill.
+///
+/// Skills are located in:
+/// - Global: `~/.config/zed/skills/<skill-name>/`
+/// - Worktree-specific: `<worktree>/.agents/skills/<skill-name>/`
+///
+/// Each skill contains:
+/// - `SKILL.md` - Main instructions
+/// - `scripts/` - Executable scripts
+/// - `references/` - Additional documentation
+/// - `assets/` - Templates, data files, images
+///
+/// To use a skill, first read its SKILL.md file, then explore its resources as needed.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct SkillToolInput {
+    /// The path to read or list within a skills directory.
+    /// Use `~` for the home directory prefix.
+    ///
+    /// Examples:
+    /// - `~/.config/zed/skills/brainstorming/SKILL.md`
+    /// - `~/.config/zed/skills/brainstorming/references/`
+    pub path: String,
+}
+
+pub struct SkillTool {
+    project: Entity<Project>,
+}
+
+impl SkillTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for SkillTool {
+    type Input = SkillToolInput;
+    type Output = String;
+
+    const NAME: &'static str = "read_skill";
+
+    fn kind() -> acp::ToolKind {
+        acp::ToolKind::Read
+    }
+
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
+        if let Ok(input) = input {
+            let path = std::path::Path::new(&input.path);
+            let skill_name = path
+                .components()
+                .rev()
+                .find_map(|component| {
+                    let name = component.as_os_str().to_str()?;
+                    if name == "skills"
+                        || name == ".agents"
+                        || name == ".config"
+                        || name == "zed"
+                        || name == "~"
+                        || name == "SKILL.md"
+                        || name == "references"
+                        || name == "scripts"
+                        || name == "assets"
+                    {
+                        None
+                    } else {
+                        Some(name.to_string())
+                    }
+                })
+                .unwrap_or_default();
+
+            if skill_name.is_empty() {
+                "Read skill".into()
+            } else {
+                format!("Read skill `{skill_name}`").into()
+            }
+        } else {
+            "Read skill".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: ToolInput<Self::Input>,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<String, String>> {
+        let project = self.project.clone();
+        cx.spawn(async move |cx| {
+            let input = input
+                .recv()
+                .await
+                .map_err(|e| format!("Failed to receive tool input: {e}"))?;
+
+            let fs = project.read_with(cx, |project, _cx| project.fs().clone());
+            let canonical_roots = canonicalize_worktree_roots(&project, &fs, cx).await;
+
+            let canonical_path = crate::skills::is_skills_path(&input.path, &canonical_roots)
+                .ok_or_else(|| {
+                    format!(
+                        "Path {} is not within a skills directory. \
+                         Skills are located at ~/.config/zed/skills/<skill-name>/ \
+                         or <worktree>/.agents/skills/<skill-name>/",
+                        input.path
+                    )
+                })?;
+
+            if fs.is_file(&canonical_path).await {
+                fs.load(&canonical_path)
+                    .await
+                    .map_err(|e| format!("Failed to read {}: {e}", input.path))
+            } else if fs.is_dir(&canonical_path).await {
+                let mut entries = fs
+                    .read_dir(&canonical_path)
+                    .await
+                    .map_err(|e| format!("Failed to list {}: {e}", input.path))?;
+
+                let mut folders = Vec::new();
+                let mut files = Vec::new();
+
+                while let Some(entry) = entries.next().await {
+                    let path = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
+                    let name = path
+                        .file_name()
+                        .unwrap_or_default()
+                        .to_string_lossy()
+                        .into_owned();
+                    if fs.is_dir(&path).await {
+                        folders.push(name);
+                    } else {
+                        files.push(name);
+                    }
+                }
+
+                folders.sort();
+                files.sort();
+
+                let mut output = String::new();
+                if !folders.is_empty() {
+                    writeln!(output, "# Folders:\n{}", folders.join("\n"))
+                        .map_err(|e| e.to_string())?;
+                }
+                if !files.is_empty() {
+                    writeln!(output, "\n# Files:\n{}", files.join("\n"))
+                        .map_err(|e| e.to_string())?;
+                }
+                if output.is_empty() {
+                    output = format!("{} is empty.", input.path);
+                }
+
+                Ok(output)
+            } else {
+                Err(format!("Path not found: {}", input.path))
+            }
+        })
+    }
+}

crates/feature_flags/src/flags.rs 🔗

@@ -67,6 +67,16 @@ impl FeatureFlag for UpdatePlanToolFeatureFlag {
     }
 }
 
+pub struct AgentSkillsFeatureFlag;
+
+impl FeatureFlag for AgentSkillsFeatureFlag {
+    const NAME: &'static str = "agent-skills";
+
+    fn enabled_for_staff() -> bool {
+        false
+    }
+}
+
 pub struct ProjectPanelUndoRedoFeatureFlag;
 
 impl FeatureFlag for ProjectPanelUndoRedoFeatureFlag {