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"
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.
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(-)
@@ -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"
@@ -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,
@@ -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
@@ -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);
@@ -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,
@@ -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))
+ }
+ })
+ }
+}
@@ -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 {