diff --git a/Cargo.toml b/Cargo.toml index df6374282db12b47af81485258758cd8fbb961e3..a12eb59879ddece9aa4ac609ae7a46361f7480aa 100644 --- a/Cargo.toml +++ b/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" diff --git a/assets/settings/default.json b/assets/settings/default.json index e9d21eb0dcc18ae939a41e3415b93eaeba1e4546..0aadc70bb70d6649291446c88842d99d892ebe49 100644 --- a/assets/settings/default.json +++ b/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, diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 8e1251a35b96f55b09afe5b8bf7c9fb53a8a88b0..d6dea211daf7d72523412c1bc663502c903111c9 100644 --- a/crates/agent/Cargo.toml +++ b/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 diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index c7c6ac7642024ffbff47d42ba390e9f78e177bf7..8978701cc78074c432f9213d8250148efc12d26f 100644 --- a/crates/agent/src/thread.rs +++ b/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::() { + 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); diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs index f3a6ac7ec6d139a2f464ce5ca4229ffdb4564714..025ab6d77c891505a66932d9e5e753eb3342001c 100644 --- a/crates/agent/src/tools.rs +++ b/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, diff --git a/crates/agent/src/tools/skill_tool.rs b/crates/agent/src/tools/skill_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..f6adfca72b40bafeac2d4068823163ca60659626 --- /dev/null +++ b/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//` +/// - Worktree-specific: `/.agents/skills//` +/// +/// 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, +} + +impl SkillTool { + pub fn new(project: Entity) -> 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, + _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, + input: ToolInput, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + 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// \ + or /.agents/skills//", + 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)) + } + }) + } +} diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index 54dc96ad37f8e51a1074a0a32976f8236cb1a0ed..7ab17ac40eaa2aa4b3e7d1888978c496c66c38a6 100644 --- a/crates/feature_flags/src/flags.rs +++ b/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 {