mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
refactor: skills as its own platform ext (#8244)
Some checks are pending
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-intel (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Unused Dependencies / machete (push) Waiting to run
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Build Rust Project on Windows (push) Waiting to run
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check OpenAPI Schema is Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Deploy Documentation / deploy (push) Waiting to run
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Live Provider Tests / goose server HTTP integration tests (push) Blocked by required conditions
Publish Ask AI Bot Docker Image / docker (push) Waiting to run
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Some checks are pending
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-intel (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Unused Dependencies / machete (push) Waiting to run
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Build Rust Project on Windows (push) Waiting to run
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check OpenAPI Schema is Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Deploy Documentation / deploy (push) Waiting to run
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Live Provider Tests / goose server HTTP integration tests (push) Blocked by required conditions
Publish Ask AI Bot Docker Image / docker (push) Waiting to run
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
This commit is contained in:
parent
6a594a2094
commit
fb04c8973b
6 changed files with 591 additions and 833 deletions
|
|
@ -424,19 +424,13 @@ pub async fn get_slash_commands(
|
|||
|
||||
let working_dir = query.working_dir.map(std::path::PathBuf::from);
|
||||
for source in
|
||||
goose::agents::platform_extensions::summon::list_installed_sources(working_dir.as_deref())
|
||||
goose::agents::platform_extensions::skills::list_installed_skills(working_dir.as_deref())
|
||||
{
|
||||
if matches!(
|
||||
source.kind,
|
||||
goose::agents::platform_extensions::summon::SourceKind::Skill
|
||||
| goose::agents::platform_extensions::summon::SourceKind::BuiltinSkill
|
||||
) {
|
||||
commands.push(SlashCommand {
|
||||
command: source.name,
|
||||
help: source.description,
|
||||
command_type: CommandType::Skill,
|
||||
});
|
||||
}
|
||||
commands.push(SlashCommand {
|
||||
command: source.name,
|
||||
help: source.description,
|
||||
command_type: CommandType::Skill,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(SlashCommandsResponse { commands }))
|
||||
|
|
|
|||
|
|
@ -135,7 +135,8 @@ impl Agent {
|
|||
}
|
||||
|
||||
async fn handle_skills_command(&self, session_id: &str) -> Result<Option<Message>> {
|
||||
use super::platform_extensions::summon::{list_installed_sources, SourceKind};
|
||||
use super::platform_extensions::skills::list_installed_skills;
|
||||
use super::platform_extensions::SourceKind;
|
||||
|
||||
let working_dir = self
|
||||
.config
|
||||
|
|
@ -144,7 +145,7 @@ impl Agent {
|
|||
.await
|
||||
.ok()
|
||||
.map(|s| s.working_dir);
|
||||
let sources = list_installed_sources(working_dir.as_deref());
|
||||
let sources = list_installed_skills(working_dir.as_deref());
|
||||
let skills: Vec<_> = sources
|
||||
.iter()
|
||||
.filter(|s| matches!(s.kind, SourceKind::Skill | SourceKind::BuiltinSkill))
|
||||
|
|
|
|||
|
|
@ -6,16 +6,79 @@ pub mod code_execution;
|
|||
pub mod developer;
|
||||
pub mod ext_manager;
|
||||
pub mod orchestrator;
|
||||
pub mod skills;
|
||||
pub mod summarize;
|
||||
pub mod summon;
|
||||
pub mod todo;
|
||||
pub mod tom;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::agents::mcp_client::McpClientTrait;
|
||||
use crate::session::Session;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Source {
|
||||
pub name: String,
|
||||
pub kind: SourceKind,
|
||||
pub description: String,
|
||||
pub path: PathBuf,
|
||||
pub content: String,
|
||||
pub supporting_files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SourceKind {
|
||||
Subrecipe,
|
||||
Recipe,
|
||||
Skill,
|
||||
Agent,
|
||||
BuiltinSkill,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SourceKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SourceKind::Subrecipe => write!(f, "subrecipe"),
|
||||
SourceKind::Recipe => write!(f, "recipe"),
|
||||
SourceKind::Skill => write!(f, "skill"),
|
||||
SourceKind::Agent => write!(f, "agent"),
|
||||
SourceKind::BuiltinSkill => write!(f, "builtin skill"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn to_load_text(&self) -> String {
|
||||
format!(
|
||||
"## {} ({})\n\n{}\n\n### Content\n\n{}",
|
||||
self.name, self.kind, self.description, self.content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_frontmatter<T: for<'de> Deserialize<'de>>(content: &str) -> Option<(T, String)> {
|
||||
let parts: Vec<&str> = content.split("---").collect();
|
||||
if parts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let yaml_content = parts[1].trim();
|
||||
let metadata: T = match serde_yaml::from_str(yaml_content) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse frontmatter: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let body = parts[2..].join("---").trim().to_string();
|
||||
Some((metadata, body))
|
||||
}
|
||||
|
||||
pub use ext_manager::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
|
||||
|
||||
|
|
@ -189,6 +252,19 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
|
|||
},
|
||||
);
|
||||
|
||||
map.insert(
|
||||
skills::EXTENSION_NAME,
|
||||
PlatformExtensionDef {
|
||||
name: skills::EXTENSION_NAME,
|
||||
display_name: "Skills",
|
||||
description: "Discover and provide skill instructions from filesystem and builtins",
|
||||
default_enabled: true,
|
||||
unprefixed_tools: false,
|
||||
hidden: true,
|
||||
client_factory: |ctx| Box::new(skills::SkillsClient::new(ctx).unwrap()),
|
||||
},
|
||||
);
|
||||
|
||||
map
|
||||
},
|
||||
);
|
||||
|
|
|
|||
410
crates/goose/src/agents/platform_extensions/skills.rs
Normal file
410
crates/goose/src/agents/platform_extensions/skills.rs
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
use super::{parse_frontmatter, Source, SourceKind};
|
||||
use crate::agents::builtin_skills;
|
||||
use crate::agents::extension::PlatformExtensionContext;
|
||||
use crate::agents::mcp_client::{Error, McpClientTrait};
|
||||
use crate::agents::tool_execution::ToolCallContext;
|
||||
use crate::config::paths::Paths;
|
||||
use async_trait::async_trait;
|
||||
use rmcp::model::{
|
||||
CallToolResult, Implementation, InitializeResult, JsonObject, ListToolsResult,
|
||||
ServerCapabilities, ServerNotification,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::warn;
|
||||
|
||||
pub static EXTENSION_NAME: &str = "skills";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SkillMetadata {
|
||||
name: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
pub fn parse_skill_content(content: &str, path: PathBuf) -> Option<Source> {
|
||||
let (metadata, body): (SkillMetadata, String) = parse_frontmatter(content)?;
|
||||
|
||||
if metadata.name.contains('/') {
|
||||
warn!(
|
||||
"Skill name '{}' contains '/' which is not allowed, skipping",
|
||||
metadata.name
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Source {
|
||||
name: metadata.name,
|
||||
kind: SourceKind::Skill,
|
||||
description: metadata.description,
|
||||
path,
|
||||
content: body,
|
||||
supporting_files: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn scan_skills_from_dir(dir: &Path, seen: &mut HashSet<String>) -> Vec<Source> {
|
||||
let mut sources = Vec::new();
|
||||
let mut visited_dirs = HashSet::new();
|
||||
for skill_file in collect_skill_files(dir, &mut visited_dirs) {
|
||||
let Some(skill_dir) = skill_file.parent() else {
|
||||
continue;
|
||||
};
|
||||
let content = match std::fs::read_to_string(&skill_file) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Failed to read skill file {}: {}", skill_file.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(mut source) = parse_skill_content(&content, skill_dir.to_path_buf()) {
|
||||
if !seen.contains(&source.name) {
|
||||
let mut visited_support_dirs = HashSet::new();
|
||||
source.supporting_files =
|
||||
find_supporting_files(skill_dir, &mut visited_support_dirs);
|
||||
seen.insert(source.name.clone());
|
||||
sources.push(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
fn collect_skill_files(dir: &Path, visited_dirs: &mut HashSet<PathBuf>) -> Vec<PathBuf> {
|
||||
let mut skill_files = Vec::new();
|
||||
|
||||
walk_files_recursively(
|
||||
dir,
|
||||
visited_dirs,
|
||||
&mut |path| !should_skip_dir(path),
|
||||
&mut |path| {
|
||||
if path.file_name().and_then(|name| name.to_str()) == Some("SKILL.md") {
|
||||
skill_files.push(path.to_path_buf());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
skill_files
|
||||
}
|
||||
|
||||
fn should_skip_dir(path: &Path) -> bool {
|
||||
matches!(
|
||||
path.file_name().and_then(|name| name.to_str()),
|
||||
Some(".git") | Some(".hg") | Some(".svn")
|
||||
)
|
||||
}
|
||||
|
||||
fn walk_files_recursively<F, G>(
|
||||
dir: &Path,
|
||||
visited_dirs: &mut HashSet<PathBuf>,
|
||||
should_descend: &mut G,
|
||||
visit_file: &mut F,
|
||||
) where
|
||||
F: FnMut(&Path),
|
||||
G: FnMut(&Path) -> bool,
|
||||
{
|
||||
let canonical_dir = match std::fs::canonicalize(dir) {
|
||||
Ok(path) => path,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if !visited_dirs.insert(canonical_dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entries = match std::fs::read_dir(dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
if should_descend(&path) {
|
||||
walk_files_recursively(&path, visited_dirs, should_descend, visit_file);
|
||||
}
|
||||
} else if path.is_file() {
|
||||
visit_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_supporting_files(
|
||||
directory: &Path,
|
||||
visited_dirs: &mut HashSet<PathBuf>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
walk_files_recursively(
|
||||
directory,
|
||||
visited_dirs,
|
||||
&mut |path| !should_skip_dir(path) && !path.join("SKILL.md").is_file(),
|
||||
&mut |path| {
|
||||
let is_skill_md = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n == "SKILL.md")
|
||||
.unwrap_or(false);
|
||||
if !is_skill_md {
|
||||
files.push(path.to_path_buf());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
fn skill_dirs(working_dir: &Path) -> (Vec<PathBuf>, Vec<PathBuf>) {
|
||||
let home = dirs::home_dir();
|
||||
let config = Paths::config_dir();
|
||||
|
||||
let local = vec![
|
||||
working_dir.join(".goose/skills"),
|
||||
working_dir.join(".claude/skills"),
|
||||
working_dir.join(".agents/skills"),
|
||||
];
|
||||
|
||||
let global = [
|
||||
home.as_ref().map(|h| h.join(".agents/skills")),
|
||||
Some(config.join("skills")),
|
||||
home.as_ref().map(|h| h.join(".claude/skills")),
|
||||
home.as_ref().map(|h| h.join(".config/agents/skills")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
(local, global)
|
||||
}
|
||||
|
||||
pub fn discover_skills(working_dir: &Path) -> Vec<Source> {
|
||||
let mut sources = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
let (local_dirs, global_dirs) = skill_dirs(working_dir);
|
||||
|
||||
for dir in local_dirs {
|
||||
sources.extend(scan_skills_from_dir(&dir, &mut seen));
|
||||
}
|
||||
|
||||
for dir in global_dirs {
|
||||
sources.extend(scan_skills_from_dir(&dir, &mut seen));
|
||||
}
|
||||
|
||||
for content in builtin_skills::get_all() {
|
||||
if let Some(source) = parse_skill_content(content, PathBuf::new()) {
|
||||
if !seen.contains(&source.name) {
|
||||
seen.insert(source.name.clone());
|
||||
sources.push(Source {
|
||||
kind: SourceKind::BuiltinSkill,
|
||||
..source
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sources
|
||||
}
|
||||
|
||||
pub fn list_installed_skills(working_dir: Option<&Path>) -> Vec<Source> {
|
||||
let dir = working_dir
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
||||
discover_skills(&dir)
|
||||
}
|
||||
|
||||
fn build_skill_instructions(skills: &[&Source]) -> String {
|
||||
let mut instructions = String::new();
|
||||
if !skills.is_empty() {
|
||||
instructions.push_str(
|
||||
"\n\nYou have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them:",
|
||||
);
|
||||
for skill in skills {
|
||||
instructions.push_str(&format!("\n• {} - {}", skill.name, skill.description));
|
||||
}
|
||||
}
|
||||
instructions
|
||||
}
|
||||
|
||||
pub struct SkillsClient {
|
||||
info: InitializeResult,
|
||||
}
|
||||
|
||||
impl SkillsClient {
|
||||
pub fn new(context: PlatformExtensionContext) -> anyhow::Result<Self> {
|
||||
let instructions = if let Some(session) = &context.session {
|
||||
let sources = discover_skills(&session.working_dir);
|
||||
let mut skills: Vec<&Source> = sources
|
||||
.iter()
|
||||
.filter(|s| s.kind == SourceKind::Skill || s.kind == SourceKind::BuiltinSkill)
|
||||
.collect();
|
||||
skills.sort_by(|a, b| (&a.name, &a.path).cmp(&(&b.name, &b.path)));
|
||||
build_skill_instructions(&skills)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let info = InitializeResult::new(ServerCapabilities::builder().build())
|
||||
.with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Skills"))
|
||||
.with_instructions(instructions);
|
||||
|
||||
Ok(Self { info })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl McpClientTrait for SkillsClient {
|
||||
async fn list_tools(
|
||||
&self,
|
||||
_session_id: &str,
|
||||
_next_cursor: Option<String>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<ListToolsResult, Error> {
|
||||
Ok(ListToolsResult {
|
||||
tools: vec![],
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn call_tool(
|
||||
&self,
|
||||
_ctx: &ToolCallContext,
|
||||
name: &str,
|
||||
_arguments: Option<JsonObject>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<CallToolResult, Error> {
|
||||
Ok(CallToolResult::error(vec![rmcp::model::Content::text(
|
||||
format!("Error: Unknown tool: {}", name),
|
||||
)]))
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Option<&InitializeResult> {
|
||||
Some(&self.info)
|
||||
}
|
||||
|
||||
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification> {
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_parse_skill_content() {
|
||||
let skill = "---\nname: test-skill\ndescription: A test skill\n---\nSkill body here.";
|
||||
let source = parse_skill_content(skill, PathBuf::new()).unwrap();
|
||||
assert_eq!(source.name, "test-skill");
|
||||
assert_eq!(source.kind, SourceKind::Skill);
|
||||
assert!(source.content.contains("Skill body"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_skill_rejects_slash_in_name() {
|
||||
let skill = "---\nname: bad/skill\ndescription: A skill\n---\nContent.";
|
||||
assert!(parse_skill_content(skill, PathBuf::new()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_skill_rejects_invalid_frontmatter() {
|
||||
assert!(parse_skill_content("no frontmatter", PathBuf::new()).is_none());
|
||||
assert!(parse_skill_content("---\nunclosed", PathBuf::new()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discover_skills_from_filesystem() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let goose_skill = temp_dir.path().join(".goose/skills/my-skill");
|
||||
fs::create_dir_all(&goose_skill).unwrap();
|
||||
fs::write(
|
||||
goose_skill.join("SKILL.md"),
|
||||
"---\nname: my-skill\ndescription: goose version\n---\nContent",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let claude_skill = temp_dir.path().join(".claude/skills/my-skill");
|
||||
fs::create_dir_all(&claude_skill).unwrap();
|
||||
fs::write(
|
||||
claude_skill.join("SKILL.md"),
|
||||
"---\nname: my-skill\ndescription: claude version\n---\nContent",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sources = discover_skills(temp_dir.path());
|
||||
let skill = sources.iter().find(|s| s.name == "my-skill").unwrap();
|
||||
assert_eq!(skill.description, "goose version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discover_skills_includes_builtins() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let sources = discover_skills(temp_dir.path());
|
||||
assert!(sources.iter().any(|s| s.kind == SourceKind::BuiltinSkill));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skill_supporting_files() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let skill_dir = temp_dir.path().join(".goose/skills/my-skill");
|
||||
fs::create_dir_all(skill_dir.join("templates/nested")).unwrap();
|
||||
fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
"---\nname: my-skill\ndescription: A skill\n---\nContent",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(skill_dir.join("myscript.sh"), "#!/bin/bash\necho ok").unwrap();
|
||||
fs::write(skill_dir.join("templates/report.txt"), "template").unwrap();
|
||||
fs::write(skill_dir.join("templates/nested/checklist.txt"), "nested").unwrap();
|
||||
|
||||
let sources = discover_skills(temp_dir.path());
|
||||
let skill = sources.iter().find(|s| s.name == "my-skill").unwrap();
|
||||
assert_eq!(skill.supporting_files.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_skill_instructions_empty() {
|
||||
let empty: &[&Source] = &[];
|
||||
assert_eq!(build_skill_instructions(empty), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_skill_instructions_with_skills() {
|
||||
let skill = Source {
|
||||
name: "test".to_string(),
|
||||
kind: SourceKind::Skill,
|
||||
description: "A test skill".to_string(),
|
||||
path: PathBuf::new(),
|
||||
content: String::new(),
|
||||
supporting_files: vec![],
|
||||
};
|
||||
let instructions = build_skill_instructions(&[&skill]);
|
||||
assert!(instructions.contains("test - A test skill"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_skills_client_no_tools() {
|
||||
let context = PlatformExtensionContext {
|
||||
extension_manager: None,
|
||||
session_manager: Arc::new(crate::session::SessionManager::instance()),
|
||||
session: None,
|
||||
};
|
||||
let client = SkillsClient::new(context).unwrap();
|
||||
let result = client
|
||||
.list_tools("test", None, CancellationToken::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(result.tools.is_empty());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -93,16 +93,19 @@ Use write and edit to efficiently make changes. Test and verify as appropriate.
|
|||
|
||||
### Instructions
|
||||
Manage agent sessions: list, view, start, send messages, and interrupt agents.
|
||||
## summarize
|
||||
|
||||
|
||||
## summon
|
||||
## skills
|
||||
|
||||
### Instructions
|
||||
|
||||
|
||||
You have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them:
|
||||
• goose-doc-guide - Reference goose documentation to create, configure, or explain goose-specific features like recipes, extensions, sessions, and providers. You MUST fetch relevant goose docs before answering. You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands.
|
||||
## summarize
|
||||
|
||||
|
||||
## summon
|
||||
|
||||
|
||||
## todo
|
||||
|
||||
### Instructions
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue