Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions crates/goose-sdk/src/custom_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ pub enum SourceType {
Recipe,
Subrecipe,
Agent,
Project,
}

impl std::fmt::Display for SourceType {
Expand All @@ -607,6 +608,7 @@ impl std::fmt::Display for SourceType {
SourceType::Recipe => write!(f, "recipe"),
SourceType::Subrecipe => write!(f, "subrecipe"),
SourceType::Agent => write!(f, "agent"),
SourceType::Project => write!(f, "project"),
}
}
}
Expand All @@ -621,16 +623,22 @@ pub struct SourceEntry {
pub name: String,
pub description: String,
pub content: String,
/// Absolute path to the source on disk. A directory for skills, a file for
/// recipes and agents.
pub directory: String,
/// Stable on-disk path identifying this source. Pass it back to
/// update/delete/export to operate on this entry. The shape varies by
/// source type: skills use the directory containing `SKILL.md`, projects
/// and recipes use the markdown file path itself.
pub path: String,
/// True when the source lives in the user's global sources directory; false
/// when it lives inside a specific project.
pub global: bool,
/// Paths (absolute) of additional files that live alongside the source.
/// Only skills currently populate this; empty for other source types.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supporting_files: Vec<String>,
/// Arbitrary key/value pairs for type-specific metadata (e.g. icon, color,
/// preferredProvider for projects). Stored in the frontmatter.
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub properties: std::collections::HashMap<String, serde_json::Value>,
}

impl SourceEntry {
Expand Down Expand Up @@ -659,6 +667,14 @@ pub struct CreateSourceRequest {
/// Absolute path to the project root. Required when `global` is false.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_dir: Option<String>,
/// Project source ID. When set with `global: false`, the backend resolves
/// the project's first working directory automatically. Takes precedence
/// over `project_dir`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
/// Arbitrary key/value metadata.
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub properties: std::collections::HashMap<String, serde_json::Value>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
Expand All @@ -680,6 +696,10 @@ pub struct ListSourcesRequest {
pub source_type: Option<SourceType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_dir: Option<String>,
/// When true, also scan the working directories of all known projects for
/// project-scoped sources (e.g. skills stored under `{workingDir}/.agents/skills/`).
#[serde(default)]
pub include_project_sources: bool,
Comment on lines +699 to +702
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Regenerate ACP schema for includeProjectSources

ListSourcesRequest now defines include_project_sources in Rust, but the ACP schema/generated SDK contract still omits this field, so typed clients cannot request project-scoped source discovery without unsafe casts. This creates a contract drift between server behavior and published client types; update/regenerate schema artifacts to include includeProjectSources.

Useful? React with 👍 / 👎.

}

#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
Expand All @@ -699,6 +719,8 @@ pub struct UpdateSourceRequest {
pub name: String,
pub description: String,
pub content: String,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub properties: std::collections::HashMap<String, serde_json::Value>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
Expand Down
36 changes: 32 additions & 4 deletions crates/goose/acp-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,18 @@
"null"
],
"description": "Absolute path to the project root. Required when `global` is false."
},
"projectId": {
"type": [
"string",
"null"
],
"description": "Project source ID. When set with `global: false`, the backend resolves\nthe project's first working directory automatically. Takes precedence\nover `project_dir`."
},
"properties": {
Comment on lines 1480 to +1491
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add projectId to CreateSourceRequest schema

The ACP schema entry for CreateSourceRequest was updated for properties but still omits the new projectId request field introduced in Rust. That leaves generated SDK clients/types unable to send projectId, so typed consumers cannot use project-scoped source creation through the new convenience path.

Useful? React with 👍 / 👎.

"type": "object",
"additionalProperties": {},
"description": "Arbitrary key/value metadata."
}
},
"required": [
Expand All @@ -1500,7 +1512,8 @@
"builtinSkill",
"recipe",
"subrecipe",
"agent"
"agent",
"project"
],
"description": "The type of source entity."
},
Expand Down Expand Up @@ -1532,9 +1545,9 @@
"content": {
"type": "string"
},
"directory": {
"path": {
"type": "string",
"description": "Absolute path to the source on disk. A directory for skills, a file for\nrecipes and agents."
"description": "Stable on-disk path identifying this source. Pass it back to\nupdate/delete/export to operate on this entry. The shape varies by\nsource type: skills use the directory containing `SKILL.md`, projects\nand recipes use the markdown file path itself."
},
"global": {
"type": "boolean",
Expand All @@ -1546,14 +1559,19 @@
"type": "string"
},
"description": "Paths (absolute) of additional files that live alongside the source.\nOnly skills currently populate this; empty for other source types."
},
"properties": {
"type": "object",
"additionalProperties": {},
"description": "Arbitrary key/value pairs for type-specific metadata (e.g. icon, color,\npreferredProvider for projects). Stored in the frontmatter."
}
},
"required": [
"type",
"name",
"description",
"content",
"directory",
"path",
"global"
],
"description": "A source discovered by Goose and backed by an on-disk path. Sources may be\neither `global` (shared across all projects) or project-specific."
Expand All @@ -1576,6 +1594,11 @@
"string",
"null"
]
},
"includeProjectSources": {
"type": "boolean",
"description": "When true, also scan the working directories of all known projects for\nproject-scoped sources (e.g. skills stored under `{workingDir}/.agents/skills/`).",
"default": false
}
},
"description": "List discovered sources.\n\nToday this endpoint only returns skills. If `type` is omitted, it defaults\nto listing skill sources. Both global and project-scoped skills are included\nwhen `project_dir` is set.",
Expand Down Expand Up @@ -1615,6 +1638,11 @@
},
"content": {
"type": "string"
},
"properties": {
"type": "object",
"additionalProperties": {},
"description": "Arbitrary key/value metadata. When non-empty, replaces all existing\nproperties on the source. Used for type-specific fields like project\nicon/color/workingDirs."
}
},
"required": [
Expand Down
2 changes: 1 addition & 1 deletion crates/goose/src/acp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2502,7 +2502,7 @@ impl GooseAcpAgent {
})
}

async fn update_thread_metadata(
pub(super) async fn update_thread_metadata(
&self,
thread_id: &str,
f: impl FnOnce(&mut crate::session::ThreadMetadata),
Expand Down
22 changes: 20 additions & 2 deletions crates/goose/src/acp/server/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@ impl GooseAcpAgent {
&self,
req: CreateSourceRequest,
) -> Result<CreateSourceResponse, sacp::Error> {
let project_dir = match (&req.project_id, &req.project_dir) {
(Some(pid), _) if !req.global => {
let dirs = crate::sources::project_working_dirs(pid);
Some(dirs.into_iter().next().ok_or_else(|| {
sacp::Error::invalid_params().data(format!(
"Project \"{pid}\" has no working directories configured"
))
})?)
}
(_, Some(pd)) => Some(pd.clone()),
_ => None,
};
let source = crate::sources::create_source(
req.source_type,
&req.name,
&req.description,
&req.content,
req.global,
req.project_dir.as_deref(),
project_dir.as_deref(),
req.properties,
)?;
Ok(CreateSourceResponse { source })
}
Expand All @@ -20,7 +33,11 @@ impl GooseAcpAgent {
&self,
req: ListSourcesRequest,
) -> Result<ListSourcesResponse, sacp::Error> {
let sources = crate::sources::list_sources(req.source_type, req.project_dir.as_deref())?;
let sources = crate::sources::list_sources(
req.source_type,
req.project_dir.as_deref(),
req.include_project_sources,
)?;
Ok(ListSourcesResponse { sources })
}

Expand All @@ -34,6 +51,7 @@ impl GooseAcpAgent {
&req.name,
&req.description,
&req.content,
req.properties,
)?;
Ok(UpdateSourceResponse { source })
}
Expand Down
23 changes: 23 additions & 0 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,24 @@ impl Agent {
messages
}

async fn load_project_instructions(&self, session: &Session) -> Option<String> {
let thread_id = session.thread_id.as_deref()?;
let thread_mgr =
crate::session::ThreadManager::new(self.config.session_manager.storage().clone());
let thread = thread_mgr.get_thread(thread_id).await.ok()?;
let project_id = thread.metadata.project_id.as_deref()?;
let entry = crate::sources::read_project(project_id).ok()?;
let mut parts = Vec::new();
parts.push(format!("# Project: {}", entry.name));
if !entry.description.is_empty() {
parts.push(entry.description.clone());
}
if !entry.content.is_empty() {
parts.push(entry.content.clone());
}
Some(parts.join("\n\n"))
}

async fn prepare_reply_context(
&self,
session_id: &str,
Expand Down Expand Up @@ -1247,6 +1265,11 @@ impl Agent {
goose_mode,
initial_messages,
} = context;

if let Some(project_addendum) = self.load_project_instructions(&session).await {
system_prompt = format!("{system_prompt}\n\n{project_addendum}");
}

self.reset_retry_attempts().await;

let provider = self.provider().await?;
Expand Down
15 changes: 9 additions & 6 deletions crates/goose/src/agents/platform_extensions/summon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ fn parse_agent_content(content: &str, path: &Path) -> Option<SourceEntry> {
name: metadata.name,
description,
content: body,
directory: path.to_string_lossy().into_owned(),
path: path.to_string_lossy().into_owned(),
global: false,
supporting_files: Vec::new(),
properties: std::collections::HashMap::new(),
})
}

Expand Down Expand Up @@ -171,9 +172,10 @@ fn scan_recipes_from_dir(
name,
description: recipe.description.clone(),
content: recipe.instructions.clone().unwrap_or_default(),
directory: path.to_string_lossy().into_owned(),
path: path.to_string_lossy().into_owned(),
global: false,
supporting_files: Vec::new(),
properties: std::collections::HashMap::new(),
});
}
Err(e) => {
Expand Down Expand Up @@ -598,9 +600,10 @@ impl SummonClient {
name: sr.name.clone(),
description,
content: String::new(),
directory: sr.path.clone(),
path: sr.path.clone(),
global: false,
supporting_files: Vec::new(),
properties: std::collections::HashMap::new(),
});
}
}
Expand Down Expand Up @@ -1160,7 +1163,7 @@ impl SummonClient {
}
}

let recipe_file = load_local_recipe_file(&source.directory)
let recipe_file = load_local_recipe_file(&source.path)
.map_err(|e| format!("Failed to load recipe '{}': {}", source.name, e))?;

let param_values: Vec<(String, String)> = params
Expand Down Expand Up @@ -1193,10 +1196,10 @@ impl SummonClient {
source: &SourceEntry,
params: &DelegateParams,
) -> Result<Recipe, String> {
let agent_content = if source.directory.is_empty() {
let agent_content = if source.path.is_empty() {
return Err("Agent source has no path".to_string());
} else {
std::fs::read_to_string(&source.directory)
std::fs::read_to_string(&source.path)
.map_err(|e| format!("Failed to read agent file: {}", e))?
};

Expand Down
8 changes: 4 additions & 4 deletions crates/goose/src/skills/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl SkillsClient {
s.source_type == SourceType::Skill || s.source_type == SourceType::BuiltinSkill
})
.collect();
skills.sort_by(|a, b| (&a.name, &a.directory).cmp(&(&b.name, &b.directory)));
skills.sort_by(|a, b| (&a.name, &a.path).cmp(&(&b.name, &b.path)));

if !skills.is_empty() {
instructions.push_str(
Expand Down Expand Up @@ -131,10 +131,10 @@ impl McpClientTrait for SkillsClient {
);

if !skill.supporting_files.is_empty() {
let skill_dir = Path::new(&skill.directory);
let skill_dir = Path::new(&skill.path);
output.push_str(&format!(
"\n## Supporting Files\n\nSkill directory: {}\n\n",
skill.directory
skill.path
));
for file in &skill.supporting_files {
if let Ok(relative) = Path::new(file).strip_prefix(skill_dir) {
Expand All @@ -157,7 +157,7 @@ impl McpClientTrait for SkillsClient {
s.name == parent_skill_name
&& matches!(s.source_type, SourceType::Skill | SourceType::BuiltinSkill)
}) {
let skill_dir = PathBuf::from(&skill.directory);
let skill_dir = PathBuf::from(&skill.path);
let canonical_skill_dir = skill_dir
.canonicalize()
.unwrap_or_else(|_| skill_dir.clone());
Expand Down
3 changes: 2 additions & 1 deletion crates/goose/src/skills/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,10 @@ fn parse_skill_content(content: &str, path: &Path, global: bool) -> Option<Sourc
name,
description: metadata.description,
content: body,
directory: path.to_string_lossy().into_owned(),
path: path.to_string_lossy().into_owned(),
global,
supporting_files: Vec::new(),
properties: std::collections::HashMap::new(),
})
}

Expand Down
Loading
Loading