From a38922d95e7a47538a694fbf8d910d1e1e8929be Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 11 May 2026 13:33:32 -0400 Subject: [PATCH] Agents crud (#9084) --- crates/goose-cli/src/cli.rs | 14 + crates/goose-sdk/src/custom_requests.rs | 4 + crates/goose/acp-schema.json | 5 + crates/goose/src/acp/server.rs | 41 +- crates/goose/src/acp/server/sources.rs | 22 +- crates/goose/src/acp/server_factory.rs | 17 +- .../src/agents/platform_extensions/summon.rs | 3 + crates/goose/src/config/paths.rs | 7 + crates/goose/src/lib.rs | 1 + crates/goose/src/skills/mod.rs | 1 + crates/goose/src/source_roots.rs | 16 + crates/goose/src/sources.rs | 587 ++++++++++++++++-- crates/goose/tests/acp_fixtures/mod.rs | 15 +- ui/goose2/builtin-sources/agents/ralph.md | 34 + ui/goose2/builtin-sources/agents/scout.md | 52 ++ ui/goose2/builtin-sources/agents/solo.md | 210 +++++++ ui/goose2/src-tauri/Cargo.lock | 29 - ui/goose2/src-tauri/Cargo.toml | 2 - ui/goose2/src-tauri/src/commands/agents.rs | 211 +------ ui/goose2/src-tauri/src/lib.rs | 14 +- .../src-tauri/src/services/acp/goose_serve.rs | 48 +- ui/goose2/src-tauri/src/services/mod.rs | 1 - ui/goose2/src-tauri/src/services/personas.rs | 457 -------------- ui/goose2/src-tauri/src/types/agents.rs | 198 ------ .../src-tauri/src/types/builtin_personas.rs | 326 ---------- ui/goose2/src-tauri/src/types/mod.rs | 3 - ui/goose2/src-tauri/tauri.conf.json | 3 +- .../agents/lib/personaPresentation.ts | 5 +- .../src/features/agents/stores/agentStore.ts | 3 +- .../src/features/agents/ui/AgentsView.tsx | 3 +- .../src/features/agents/ui/AvatarDropZone.tsx | 37 +- .../src/features/agents/ui/PersonaCard.tsx | 10 +- .../src/features/agents/ui/PersonaEditor.tsx | 9 +- .../src/features/agents/ui/PersonaGallery.tsx | 4 +- .../features/chat/ui/MentionAutocomplete.tsx | 4 +- .../src/features/chat/ui/PersonaPicker.tsx | 8 +- .../src/shared/api/__tests__/agents.test.ts | 216 +++++-- ui/goose2/src/shared/api/agents.ts | 202 +++++- ui/goose2/src/shared/lib/avatarUrl.ts | 24 +- ui/goose2/src/shared/types/agents.ts | 25 +- ui/goose2/tests/e2e/fixtures/tauri-mock.ts | 44 +- ui/sdk/src/generated/types.gen.ts | 5 + ui/sdk/src/generated/zod.gen.ts | 1 + 43 files changed, 1463 insertions(+), 1458 deletions(-) create mode 100644 crates/goose/src/source_roots.rs create mode 100644 ui/goose2/builtin-sources/agents/ralph.md create mode 100644 ui/goose2/builtin-sources/agents/scout.md create mode 100644 ui/goose2/builtin-sources/agents/solo.md delete mode 100644 ui/goose2/src-tauri/src/services/personas.rs delete mode 100644 ui/goose2/src-tauri/src/types/agents.rs delete mode 100644 ui/goose2/src-tauri/src/types/builtin_personas.rs diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 76aeee9950..ce301cc691 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -7,6 +7,7 @@ use goose::config::{Config, GooseMode}; #[cfg(feature = "telemetry")] use goose::posthog::get_telemetry_choice; use goose::recipe::Recipe; +use goose::source_roots::SourceRoot; use goose_mcp::mcp_server_runner::{serve, McpCommand}; use goose_mcp::{AutoVisualiserRouter, ComputerControllerServer, MemoryServer, TutorialServer}; @@ -1123,11 +1124,24 @@ async fn handle_serve_command(host: String, port: u16, builtins: Vec) -> builtins }; + let additional_source_roots = Config::global() + .get_param::("ADDITIONAL_AGENT_SOURCE_ROOTS") + .ok() + .map(|paths| std::env::split_paths(&paths).collect::>()) + .unwrap_or_default() + .into_iter() + .map(|path| { + let path = path.canonicalize().unwrap_or(path); + SourceRoot::read_only(path) + }) + .collect(); + let server = Arc::new(AcpServer::new(AcpServerFactoryConfig { builtins, data_dir: Paths::data_dir(), config_dir: Paths::config_dir(), goose_platform: GoosePlatform::GooseCli, + additional_source_roots, })); let secret_key = std::env::var(GOOSE_SERVER_SECRET_KEY_ENV) .ok() diff --git a/crates/goose-sdk/src/custom_requests.rs b/crates/goose-sdk/src/custom_requests.rs index f664d3fb2c..e9626d4589 100644 --- a/crates/goose-sdk/src/custom_requests.rs +++ b/crates/goose-sdk/src/custom_requests.rs @@ -852,6 +852,10 @@ pub struct SourceEntry { /// True when the source lives in the user's global sources directory; false /// when it lives inside a specific project. pub global: bool, + /// True when this source can be modified through source CRUD methods. + /// Client-provided bundled sources are returned as read-only. + #[serde(default)] + pub writable: 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")] diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json index e9a510731d..fcace1e3c5 100644 --- a/crates/goose/acp-schema.json +++ b/crates/goose/acp-schema.json @@ -1957,6 +1957,11 @@ "type": "boolean", "description": "True when the source lives in the user's global sources directory; false\nwhen it lives inside a specific project." }, + "writable": { + "type": "boolean", + "description": "True when this source can be modified through source CRUD methods.\nClient-provided bundled sources are returned as read-only.", + "default": false + }, "supportingFiles": { "type": "array", "items": { diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index 75d74d8249..5db5f7cde3 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -23,6 +23,7 @@ use crate::providers::inventory::{ }; use crate::session::session_manager::SessionType; use crate::session::{EnabledExtensionsState, Session, SessionManager}; +use crate::source_roots::SourceRoot; use crate::utils::sanitize_unicode_tags; use agent_client_protocol::schema::{ AgentCapabilities, Annotations, AuthMethod, AuthMethodAgent, AuthenticateRequest, @@ -216,6 +217,17 @@ struct AgentSetupRequest { prebuilt_provider: Option>, } +pub struct GooseAcpAgentOptions { + pub provider_factory: AcpProviderFactory, + pub builtins: Vec, + pub data_dir: std::path::PathBuf, + pub config_dir: std::path::PathBuf, + pub goose_mode: GooseMode, + pub disable_session_naming: bool, + pub goose_platform: GoosePlatform, + pub additional_source_roots: Vec, +} + pub struct GooseAcpAgent { sessions: Arc>>, provider_factory: AcpProviderFactory, @@ -230,6 +242,7 @@ pub struct GooseAcpAgent { disable_session_naming: bool, provider_inventory: ProviderInventoryService, goose_platform: GoosePlatform, + additional_source_roots: Vec, } /// Shorten a session/thread id for perf log correlation. @@ -1036,16 +1049,8 @@ impl GooseAcpAgent { } // TODO: goose reads Paths::in_state_dir globally (e.g. RequestLog), ignoring this data_dir. - pub async fn new( - provider_factory: AcpProviderFactory, - builtins: Vec, - data_dir: std::path::PathBuf, - config_dir: std::path::PathBuf, - goose_mode: GooseMode, - disable_session_naming: bool, - goose_platform: GoosePlatform, - ) -> Result { - let session_manager = Arc::new(SessionManager::new(data_dir)); + pub async fn new(options: GooseAcpAgentOptions) -> Result { + let session_manager = Arc::new(SessionManager::new(options.data_dir)); // Eagerly initialize the SQLite pool so it's ready when providers/sessions need it. let storage_clone = session_manager.storage().clone(); @@ -1053,23 +1058,24 @@ impl GooseAcpAgent { let _ = storage_clone.pool().await; }); - let permission_manager = Arc::new(PermissionManager::new(config_dir.clone())); + let permission_manager = Arc::new(PermissionManager::new(options.config_dir.clone())); let provider_inventory = ProviderInventoryService::new(session_manager.storage().clone()); Ok(Self { sessions: Arc::new(Mutex::new(HashMap::new())), - provider_factory, - builtins, + provider_factory: options.provider_factory, + builtins: options.builtins, client_fs_capabilities: OnceCell::new(), client_terminal: OnceCell::new(), client_mcp_host_info: OnceCell::new(), - config_dir, + config_dir: options.config_dir, session_manager, permission_manager, - goose_mode, - disable_session_naming, + goose_mode: options.goose_mode, + disable_session_naming: options.disable_session_naming, provider_inventory, - goose_platform, + goose_platform: options.goose_platform, + additional_source_roots: options.additional_source_roots, }) } @@ -3315,6 +3321,7 @@ pub async fn run(builtins: Vec) -> Result<()> { data_dir: Paths::data_dir(), config_dir: Paths::config_dir(), goose_platform: GoosePlatform::GooseCli, + additional_source_roots: Vec::new(), }, ); let agent = server.create_agent().await?; diff --git a/crates/goose/src/acp/server/sources.rs b/crates/goose/src/acp/server/sources.rs index 6a9bae833f..9360e7e415 100644 --- a/crates/goose/src/acp/server/sources.rs +++ b/crates/goose/src/acp/server/sources.rs @@ -33,10 +33,11 @@ impl GooseAcpAgent { &self, req: ListSourcesRequest, ) -> Result { - let sources = crate::sources::list_sources( + let sources = crate::sources::list_sources_with_roots( req.source_type, req.project_dir.as_deref(), req.include_project_sources, + &self.additional_source_roots, )?; Ok(ListSourcesResponse { sources }) } @@ -45,13 +46,16 @@ impl GooseAcpAgent { &self, req: UpdateSourceRequest, ) -> Result { - let source = crate::sources::update_source( + let source = crate::sources::update_source_with_roots( req.source_type, &req.path, &req.name, &req.description, &req.content, - req.properties, + crate::sources::UpdateSourceOptions { + properties: req.properties, + additional_roots: &self.additional_source_roots, + }, )?; Ok(UpdateSourceResponse { source }) } @@ -60,7 +64,11 @@ impl GooseAcpAgent { &self, req: DeleteSourceRequest, ) -> Result { - crate::sources::delete_source(req.source_type, &req.path)?; + crate::sources::delete_source_with_roots( + req.source_type, + &req.path, + &self.additional_source_roots, + )?; Ok(EmptyResponse {}) } @@ -68,7 +76,11 @@ impl GooseAcpAgent { &self, req: ExportSourceRequest, ) -> Result { - let (json, filename) = crate::sources::export_source(req.source_type, &req.path)?; + let (json, filename) = crate::sources::export_source_with_roots( + req.source_type, + &req.path, + &self.additional_source_roots, + )?; Ok(ExportSourceResponse { json, filename }) } diff --git a/crates/goose/src/acp/server_factory.rs b/crates/goose/src/acp/server_factory.rs index 2e9686a708..05c745c39c 100644 --- a/crates/goose/src/acp/server_factory.rs +++ b/crates/goose/src/acp/server_factory.rs @@ -1,5 +1,6 @@ -use crate::acp::server::{AcpProviderFactory, GooseAcpAgent}; +use crate::acp::server::{AcpProviderFactory, GooseAcpAgent, GooseAcpAgentOptions}; use crate::agents::GoosePlatform; +use crate::source_roots::SourceRoot; use anyhow::Result; use std::sync::Arc; use tracing::info; @@ -9,6 +10,7 @@ pub struct AcpServerFactoryConfig { pub data_dir: std::path::PathBuf, pub config_dir: std::path::PathBuf, pub goose_platform: GoosePlatform, + pub additional_source_roots: Vec, } pub struct AcpServer { @@ -39,15 +41,16 @@ impl AcpServer { }) }); - let agent = GooseAcpAgent::new( + let agent = GooseAcpAgent::new(GooseAcpAgentOptions { provider_factory, - self.config.builtins.clone(), - self.config.data_dir.clone(), - self.config.config_dir.clone(), + builtins: self.config.builtins.clone(), + data_dir: self.config.data_dir.clone(), + config_dir: self.config.config_dir.clone(), goose_mode, disable_session_naming, - self.config.goose_platform.clone(), - ) + goose_platform: self.config.goose_platform.clone(), + additional_source_roots: self.config.additional_source_roots.clone(), + }) .await?; info!("Created new ACP agent"); diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 9ef3536800..4e599110a0 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -127,6 +127,7 @@ fn parse_agent_content(content: &str, path: &Path) -> Option { content: body, path: path.to_string_lossy().into_owned(), global: false, + writable: true, supporting_files: Vec::new(), properties: std::collections::HashMap::new(), }) @@ -174,6 +175,7 @@ fn scan_recipes_from_dir( content: recipe.instructions.clone().unwrap_or_default(), path: path.to_string_lossy().into_owned(), global: false, + writable: true, supporting_files: Vec::new(), properties: std::collections::HashMap::new(), }); @@ -602,6 +604,7 @@ impl SummonClient { content: String::new(), path: sr.path.clone(), global: false, + writable: true, supporting_files: Vec::new(), properties: std::collections::HashMap::new(), }); diff --git a/crates/goose/src/config/paths.rs b/crates/goose/src/config/paths.rs index 41441ece2f..be142869be 100644 --- a/crates/goose/src/config/paths.rs +++ b/crates/goose/src/config/paths.rs @@ -12,6 +12,7 @@ impl Paths { DirType::Data => base.join("data"), DirType::State => base.join("state"), DirType::Plugins => base.join(".agents").join("plugins"), + DirType::Agents => base.join(".agents").join("agents"), } } else { // NOTE: "Block" is kept here for backwards compatibility with existing @@ -29,6 +30,7 @@ impl Paths { DirType::Data => strategy.data_dir(), DirType::State => strategy.state_dir().unwrap_or(strategy.data_dir()), DirType::Plugins => strategy.home_dir().join(".agents").join("plugins"), + DirType::Agents => strategy.home_dir().join(".agents").join("agents"), } } } @@ -49,6 +51,10 @@ impl Paths { Self::get_dir(DirType::Plugins) } + pub fn agents_dir() -> PathBuf { + Self::get_dir(DirType::Agents) + } + pub fn in_state_dir(subpath: &str) -> PathBuf { Self::state_dir().join(subpath) } @@ -67,4 +73,5 @@ enum DirType { Data, State, Plugins, + Agents, } diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 273ac4d4d3..e1897cb84c 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -42,6 +42,7 @@ pub mod session; pub mod session_context; pub mod skills; pub mod slash_commands; +pub mod source_roots; pub mod sources; pub mod subprocess; pub mod token_counter; diff --git a/crates/goose/src/skills/mod.rs b/crates/goose/src/skills/mod.rs index 0c84d9a3e5..78e8299985 100644 --- a/crates/goose/src/skills/mod.rs +++ b/crates/goose/src/skills/mod.rs @@ -284,6 +284,7 @@ fn parse_skill_content(content: &str, path: &Path, global: bool) -> Option Self { + Self { + path, + writable: false, + } + } +} diff --git a/crates/goose/src/sources.rs b/crates/goose/src/sources.rs index 78e230a3fa..224058e587 100644 --- a/crates/goose/src/sources.rs +++ b/crates/goose/src/sources.rs @@ -8,12 +8,14 @@ use crate::skills::{ parse_skill_frontmatter, resolve_discoverable_skill_dir, resolve_skill_dir, skill_base_dir, validate_skill_name, }; +use crate::source_roots::SourceRoot; use agent_client_protocol::Error; use fs_err as fs; use goose_sdk::custom_requests::{SourceEntry, SourceType}; use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use tracing::warn; pub fn parse_frontmatter Deserialize<'de>>( content: &str, @@ -32,7 +34,7 @@ pub fn parse_frontmatter Deserialize<'de>>( fn require_mutable_type(source_type: SourceType) -> Result<(), Error> { match source_type { - SourceType::Skill | SourceType::Project => Ok(()), + SourceType::Skill | SourceType::Project | SourceType::Agent => Ok(()), other => Err(Error::invalid_params().data(format!( "Source type '{other}' is not supported for mutation." ))), @@ -44,6 +46,7 @@ fn require_listable_type(source_type: Option) -> Result Ok(SourceType::Skill), SourceType::BuiltinSkill => Ok(SourceType::BuiltinSkill), SourceType::Project => Ok(SourceType::Project), + SourceType::Agent => Ok(SourceType::Agent), other => Err(Error::invalid_params().data(format!( "Source type '{}' is not supported for listing.", other @@ -54,7 +57,7 @@ fn require_listable_type(source_type: Option) -> Result PathBuf { projects_dir().join(format!("{slug}.md")) } -fn build_project_md( +fn build_source_markdown( name: &str, description: &str, content: &str, properties: &HashMap, -) -> String { - let mut fm = serde_yaml::Mapping::new(); - fm.insert( +) -> Result { + let mut frontmatter = serde_yaml::Mapping::new(); + frontmatter.insert( serde_yaml::Value::String("name".into()), serde_yaml::Value::String(name.into()), ); - fm.insert( + frontmatter.insert( serde_yaml::Value::String("description".into()), serde_yaml::Value::String(description.into()), ); - for (k, v) in properties { - if k == "name" || k == "description" { + for (key, value) in properties { + if key == "name" || key == "description" { continue; } - if let Ok(yv) = serde_yaml::to_value(v) { - fm.insert(serde_yaml::Value::String(k.clone()), yv); - } + let value = serde_yaml::to_value(value).map_err(|e| { + Error::internal_error().data(format!("Failed to serialize source property: {e}")) + })?; + frontmatter.insert(serde_yaml::Value::String(key.clone()), value); } - let yaml = serde_yaml::to_string(&fm).unwrap_or_default(); + let yaml = serde_yaml::to_string(&frontmatter) + .map_err(|e| Error::internal_error().data(format!("Failed to serialize source: {e}")))?; let mut md = format!("---\n{yaml}---\n"); if !content.is_empty() { md.push('\n'); md.push_str(content); md.push('\n'); } - md + Ok(md) } /// Returns (display_name, description, body, properties). @@ -116,7 +121,7 @@ fn parse_project_frontmatter( HashMap::new(), ); } - match parse_frontmatter::(raw) { + match parse_frontmatter::(raw) { Ok(Some((meta, body))) => (meta.name, meta.description, body, meta.properties), _ => ( String::new(), @@ -155,6 +160,18 @@ fn read_existing_project_properties(file: &Path) -> HashMap HashMap { + let raw = match fs::read_to_string(file) { + Ok(s) => s, + Err(_) => return HashMap::new(), + }; + match parse_agent_frontmatter(&raw) { + Ok((frontmatter, _)) => frontmatter.properties, + Err(_) => HashMap::new(), + } +} + fn project_entry_from_file(file: &Path) -> Option { let slug = file.file_stem().and_then(|s| s.to_str())?.to_string(); if slug.is_empty() { @@ -182,6 +199,7 @@ fn project_entry_from_file(file: &Path) -> Option { content, path: file.to_string_lossy().into_owned(), global: true, + writable: true, supporting_files: Vec::new(), properties, }) @@ -277,6 +295,7 @@ fn skill_source_entry( content: content.to_string(), path: dir.to_string_lossy().to_string(), global, + writable: true, supporting_files: Vec::new(), properties, } @@ -290,6 +309,322 @@ fn builtin_skill_entry(mut source: SourceEntry) -> SourceEntry { source } +fn agent_base_dir(global: bool, project_dir: Option<&str>) -> Result { + if global { + Ok(Paths::agents_dir()) + } else { + let project_dir = project_dir.ok_or_else(|| { + Error::invalid_params().data("projectDir is required when global is false") + })?; + if project_dir.trim().is_empty() { + return Err( + Error::invalid_params().data("projectDir must not be empty when global is false") + ); + } + Ok(Path::new(project_dir).join(".agents").join("agents")) + } +} + +fn validate_agent_name(name: &str) -> Result<(), Error> { + let trimmed = name.trim(); + if trimmed.is_empty() { + return Err(Error::invalid_params().data("Agent name must not be empty")); + } + if trimmed.len() > 80 { + return Err(Error::invalid_params().data(format!( + "Invalid agent name \"{}\". Names must be at most 80 characters.", + name + ))); + } + if trimmed.chars().any(|ch| matches!(ch, '/' | '\\')) { + return Err(Error::invalid_params().data(format!( + "Invalid agent name \"{}\". Names must not contain path separators.", + name + ))); + } + Ok(()) +} + +fn slugify_agent_name(name: &str) -> String { + let slug: String = name + .to_lowercase() + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' }) + .collect(); + let mut collapsed = String::with_capacity(slug.len()); + let mut previous_hyphen = false; + for ch in slug.chars() { + if ch == '-' { + if !previous_hyphen { + collapsed.push('-'); + } + previous_hyphen = true; + } else { + collapsed.push(ch); + previous_hyphen = false; + } + } + let trimmed = collapsed.trim_matches('-'); + if trimmed.is_empty() { + "agent".to_string() + } else { + trimmed + .chars() + .take(64) + .collect::() + .trim_end_matches('-') + .to_string() + } +} + +fn parse_agent_frontmatter(raw: &str) -> Result<(MarkdownSourceFrontmatter, String), Error> { + parse_frontmatter::(raw) + .map_err(|e| Error::invalid_params().data(format!("Invalid agent frontmatter: {e}")))? + .ok_or_else(|| Error::invalid_params().data("Agent file is missing frontmatter")) +} + +fn agent_source_entry(path: &Path, global: bool, writable: bool) -> Result { + let raw = fs::read_to_string(path) + .map_err(|e| Error::internal_error().data(format!("Failed to read agent file: {e}")))?; + let (frontmatter, content) = parse_agent_frontmatter(&raw)?; + Ok({ + SourceEntry { + source_type: SourceType::Agent, + name: frontmatter.name, + description: frontmatter.description, + content, + path: path.to_string_lossy().to_string(), + global, + writable, + supporting_files: Vec::new(), + properties: frontmatter.properties, + } + }) +} + +fn canonicalize_or_original(path: &Path) -> PathBuf { + path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) +} + +fn is_under_root(path: &Path, root: &Path) -> bool { + canonicalize_or_original(path).starts_with(canonicalize_or_original(root)) +} + +fn is_read_only_agent_file(path: &Path, additional_roots: &[SourceRoot]) -> bool { + additional_roots + .iter() + .filter(|root| !root.writable) + .any(|root| is_under_root(path, &root.path)) +} + +fn reject_read_only_agent_file(path: &Path, additional_roots: &[SourceRoot]) -> Result<(), Error> { + if is_read_only_agent_file(path, additional_roots) { + return Err(Error::invalid_params().data("Source is read-only")); + } + Ok(()) +} + +fn is_global_agent_file(path: &Path) -> bool { + let canonical_path = canonicalize_or_original(path); + let mut global_roots = Vec::new(); + global_roots.push(Paths::agents_dir()); + if let Some(home) = dirs::home_dir() { + global_roots.push(home.join(".agents").join("agents")); + global_roots.push(home.join(".goose").join("agents")); + global_roots.push(home.join(".claude").join("agents")); + } + global_roots.push(Paths::config_dir().join("agents")); + + global_roots + .into_iter() + .any(|root| canonical_path.starts_with(canonicalize_or_original(&root))) +} + +fn resolve_agent_file_with_roots( + path: &str, + additional_roots: &[SourceRoot], +) -> Result { + if path.is_empty() { + return Err(Error::invalid_params().data("Source path must not be empty")); + } + + let canonical_file = Path::new(path) + .canonicalize() + .map_err(|_| Error::invalid_params().data(format!("Source \"{}\" not found", path)))?; + + let parent_name = canonical_file + .parent() + .and_then(Path::file_name) + .and_then(|name| name.to_str()); + let grandparent_name = canonical_file + .parent() + .and_then(Path::parent) + .and_then(Path::file_name) + .and_then(|name| name.to_str()); + let in_agent_dir = parent_name == Some("agents") + && matches!( + grandparent_name, + Some(".goose") | Some(".claude") | Some(".agents") + ); + let in_additional_root = additional_roots + .iter() + .any(|root| is_under_root(&canonical_file, &root.path)); + + if !canonical_file.is_file() + || canonical_file.extension().and_then(|ext| ext.to_str()) != Some("md") + || (!in_agent_dir && !is_global_agent_file(&canonical_file) && !in_additional_root) + { + return Err(Error::invalid_params().data(format!("Source \"{}\" not found", path))); + } + + Ok(canonical_file) +} + +fn list_agent_dirs(working_dir: Option<&Path>, additional_roots: &[SourceRoot]) -> Vec { + let mut dirs = Vec::new(); + if let Some(working_dir) = working_dir { + dirs.push(SourceRoot { + path: working_dir.join(".agents").join("agents"), + writable: true, + }); + dirs.push(SourceRoot { + path: working_dir.join(".goose").join("agents"), + writable: true, + }); + dirs.push(SourceRoot { + path: working_dir.join(".claude").join("agents"), + writable: true, + }); + } + + dirs.push(SourceRoot { + path: Paths::agents_dir(), + writable: true, + }); + if let Some(home) = dirs::home_dir() { + dirs.push(SourceRoot { + path: home.join(".agents").join("agents"), + writable: true, + }); + dirs.push(SourceRoot { + path: home.join(".goose").join("agents"), + writable: true, + }); + dirs.push(SourceRoot { + path: home.join(".claude").join("agents"), + writable: true, + }); + } + dirs.push(SourceRoot { + path: Paths::config_dir().join("agents"), + writable: true, + }); + dirs.extend(additional_roots.iter().cloned()); + dirs +} + +fn is_project_agent_file(path: &Path, working_dir: &Path) -> bool { + [".agents", ".goose", ".claude"] + .into_iter() + .map(|dir| working_dir.join(dir).join("agents")) + .any(|root| is_under_root(path, &root)) +} + +fn list_agent_sources( + project_dir: Option<&str>, + additional_roots: &[SourceRoot], +) -> Vec { + let working_dir = project_dir + .map(str::trim) + .filter(|path| !path.is_empty()) + .map(PathBuf::from); + let mut seen = std::collections::HashSet::new(); + let mut sources = Vec::new(); + + for root in list_agent_dirs(working_dir.as_deref(), additional_roots) { + let entries = match fs::read_dir(&root.path) { + Ok(entries) => entries, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("md") { + continue; + } + let global = working_dir + .as_deref() + .is_none_or(|working_dir| !is_project_agent_file(&path, working_dir)); + match agent_source_entry(&path, global, root.writable) { + Ok(source) => { + let key = source.name.to_lowercase(); + if seen.insert(key) { + sources.push(source); + } + } + Err(err) => warn!("Skipping agent source {}: {:?}", path.display(), err), + } + } + } + + sources +} + +fn create_agent_source( + name: &str, + description: &str, + content: &str, + properties: HashMap, + global: bool, + project_dir: Option<&str>, +) -> Result { + validate_agent_name(name)?; + let base = agent_base_dir(global, project_dir)?; + let slug = slugify_agent_name(name); + let mut file_path = base.join(format!("{slug}.md")); + if file_path.exists() { + let mut counter = 2u32; + loop { + file_path = base.join(format!("{slug}-{counter}.md")); + if !file_path.exists() { + break; + } + counter += 1; + } + } + + fs::create_dir_all(&base).map_err(|e| { + Error::internal_error().data(format!("Failed to create source directory: {e}")) + })?; + let md = build_source_markdown(name, description, content, &properties)?; + fs::write(&file_path, md) + .map_err(|e| Error::internal_error().data(format!("Failed to write agent file: {e}")))?; + + agent_source_entry(&file_path, global, true) +} + +fn update_agent_source( + path: &str, + name: &str, + description: &str, + content: &str, + properties: Option>, + additional_roots: &[SourceRoot], +) -> Result { + validate_agent_name(name)?; + let file_path = resolve_agent_file_with_roots(path, additional_roots)?; + reject_read_only_agent_file(&file_path, additional_roots)?; + let global = is_global_agent_file(&file_path); + let resolved_properties = match properties { + Some(p) => p, + None => read_existing_agent_properties(&file_path), + }; + let md = build_source_markdown(name, description, content, &resolved_properties)?; + fs::write(&file_path, md) + .map_err(|e| Error::internal_error().data(format!("Failed to write agent file: {e}")))?; + + agent_source_entry(&file_path, global, true) +} + // --- Public CRUD --- pub fn create_source( @@ -302,6 +637,9 @@ pub fn create_source( properties: HashMap, ) -> Result { require_mutable_type(source_type)?; + if source_type == SourceType::Agent { + return create_agent_source(name, description, content, properties, global, project_dir); + } match source_type { SourceType::Skill => { @@ -348,7 +686,7 @@ pub fn create_source( .get("title") .and_then(|v| v.as_str()) .unwrap_or(name); - let md = build_project_md(display_name, description, content, &properties); + let md = build_source_markdown(display_name, description, content, &properties)?; fs::write(&file, md).map_err(|e| { Error::internal_error().data(format!("Failed to write project file: {e}")) })?; @@ -359,15 +697,30 @@ pub fn create_source( } } -pub fn update_source( +pub struct UpdateSourceOptions<'a> { + pub properties: Option>, + pub additional_roots: &'a [SourceRoot], +} + +pub fn update_source_with_roots( source_type: SourceType, path: &str, name: &str, description: &str, content: &str, - properties: Option>, + options: UpdateSourceOptions<'_>, ) -> Result { require_mutable_type(source_type)?; + if source_type == SourceType::Agent { + return update_agent_source( + path, + name, + description, + content, + options.properties, + options.additional_roots, + ); + } match source_type { SourceType::Skill => { @@ -381,10 +734,7 @@ pub fn update_source( Error::internal_error().data("Failed to resolve source directory name") })?; - // When the caller doesn't supply properties, preserve whatever - // is already on disk so per-skill frontmatter metadata isn't - // silently erased on edit. - let resolved_properties = match properties { + let resolved_properties = match options.properties { Some(p) => p, None => read_existing_skill_properties(&dir), }; @@ -428,9 +778,6 @@ pub fn update_source( validate_project_slug(name)?; let file = resolve_project_path(path)?; - // We don't currently support renaming a project (it would change - // the slug used as the stable thread.project_id). Reject mismatches - // to surface this clearly. let current_slug = file .file_stem() .and_then(|s| s.to_str()) @@ -442,8 +789,7 @@ pub fn update_source( ))); } - // Same preserve-on-None semantics as skills. - let resolved_properties = match properties { + let resolved_properties = match options.properties { Some(p) => p, None => read_existing_project_properties(&file), }; @@ -452,7 +798,8 @@ pub fn update_source( .get("title") .and_then(|v| v.as_str()) .unwrap_or(name); - let md = build_project_md(display_name, description, content, &resolved_properties); + let md = + build_source_markdown(display_name, description, content, &resolved_properties)?; fs::write(&file, md).map_err(|e| { Error::internal_error().data(format!("Failed to write project file: {e}")) })?; @@ -464,6 +811,14 @@ pub fn update_source( } pub fn delete_source(source_type: SourceType, path: &str) -> Result<(), Error> { + delete_source_with_roots(source_type, path, &[]) +} + +pub fn delete_source_with_roots( + source_type: SourceType, + path: &str, + additional_roots: &[SourceRoot], +) -> Result<(), Error> { require_mutable_type(source_type)?; match source_type { @@ -479,6 +834,13 @@ pub fn delete_source(source_type: SourceType, path: &str) -> Result<(), Error> { Error::internal_error().data(format!("Failed to delete project: {e}")) })?; } + SourceType::Agent => { + let file_path = resolve_agent_file_with_roots(path, additional_roots)?; + reject_read_only_agent_file(&file_path, additional_roots)?; + fs::remove_file(&file_path).map_err(|e| { + Error::internal_error().data(format!("Failed to delete source: {e}")) + })?; + } _ => unreachable!("guarded by require_mutable_type"), } Ok(()) @@ -488,6 +850,15 @@ pub fn list_sources( source_type: Option, project_dir: Option<&str>, include_project_sources: bool, +) -> Result, Error> { + list_sources_with_roots(source_type, project_dir, include_project_sources, &[]) +} + +pub fn list_sources_with_roots( + source_type: Option, + project_dir: Option<&str>, + include_project_sources: bool, + additional_roots: &[SourceRoot], ) -> Result, Error> { if let Some(t) = source_type { require_listable_type(Some(t))?; @@ -564,7 +935,10 @@ pub fn list_sources( SourceType::Project => { sources.extend(read_project_dir()?); } - SourceType::Recipe | SourceType::Subrecipe | SourceType::Agent => { + SourceType::Agent => { + sources.extend(list_agent_sources(project_dir, additional_roots)); + } + SourceType::Recipe | SourceType::Subrecipe => { return Err(Error::invalid_params() .data(format!("Source type '{}' listing is not supported.", kind))); } @@ -576,6 +950,14 @@ pub fn list_sources( } pub fn export_source(source_type: SourceType, path: &str) -> Result<(String, String), Error> { + export_source_with_roots(source_type, path, &[]) +} + +pub fn export_source_with_roots( + source_type: SourceType, + path: &str, + additional_roots: &[SourceRoot], +) -> Result<(String, String), Error> { match source_type { SourceType::Skill => { let dir = resolve_discoverable_skill_dir(path)?; @@ -601,6 +983,27 @@ pub fn export_source(source_type: SourceType, path: &str) -> Result<(String, Str let filename = format!("{}.skill.json", name); Ok((json, filename)) } + SourceType::Agent => { + let file_path = resolve_agent_file_with_roots(path, additional_roots)?; + let writable = !is_read_only_agent_file(&file_path, additional_roots); + let source = agent_source_entry( + &file_path, + is_global_agent_file(&file_path) || !writable, + writable, + )?; + let export = serde_json::json!({ + "version": 1, + "type": "agent", + "name": source.name, + "description": source.description, + "content": source.content, + }); + let json = serde_json::to_string_pretty(&export).map_err(|e| { + Error::internal_error().data(format!("Failed to serialize source: {e}")) + })?; + let filename = format!("{}.agent.json", slugify_agent_name(&source.name)); + Ok((json, filename)) + } SourceType::Project => { let file = resolve_project_path(path)?; let raw = fs::read_to_string(&file).map_err(|e| { @@ -667,6 +1070,7 @@ pub fn import_sources( let source_type = match type_str { "skill" => SourceType::Skill, "project" => SourceType::Project, + "agent" => SourceType::Agent, other => { return Err(Error::invalid_params() .data(format!("Source type '{}' import is not supported.", other))); @@ -713,6 +1117,25 @@ pub fn import_sources( } } + if source_type == SourceType::Agent { + if let Some(legacy_metadata) = value.get("metadata").and_then(|v| v.as_object()) { + for (key, value) in legacy_metadata { + properties + .entry(key.clone()) + .or_insert_with(|| value.clone()); + } + } + return create_agent_source( + &name, + &description, + &content, + properties, + global, + project_dir, + ) + .map(|source| vec![source]); + } + match source_type { SourceType::Skill => { validate_skill_name(&name)?; @@ -753,7 +1176,7 @@ pub fn import_sources( &final_name, &description, &content, - true, // projects are always global + true, None, properties, ) @@ -782,6 +1205,54 @@ mod tests { assert!(validate_skill_name(&"a".repeat(65)).is_err()); } + #[test] + fn lists_additional_read_only_agent_roots() { + let tmp = TempDir::new().unwrap(); + let root = tmp.path().join("builtin").join("agents"); + std::fs::create_dir_all(&root).unwrap(); + let agent_path = root.join("solo.md"); + std::fs::write( + &agent_path, + "---\nname: Solo\ndescription: Built in\n---\n\nYou are Solo.", + ) + .unwrap(); + + let sources = list_sources_with_roots( + Some(SourceType::Agent), + None, + false, + &[SourceRoot::read_only(root.clone())], + ) + .unwrap(); + + let solo = sources.iter().find(|source| source.name == "Solo").unwrap(); + assert!(!solo.writable); + assert!(solo.global); + assert_eq!(solo.path, agent_path.to_string_lossy()); + + let err = update_source_with_roots( + SourceType::Agent, + &solo.path, + "Solo", + "Built in", + "Updated", + UpdateSourceOptions { + properties: None, + additional_roots: &[SourceRoot::read_only(root.canonicalize().unwrap())], + }, + ) + .unwrap_err(); + assert!(format!("{:?}", err).contains("read-only")); + + let err = delete_source_with_roots( + SourceType::Agent, + &solo.path, + &[SourceRoot::read_only(root.canonicalize().unwrap())], + ) + .unwrap_err(); + assert!(format!("{:?}", err).contains("read-only")); + } + #[test] fn create_list_update_delete_project_skill() { let tmp = TempDir::new().unwrap(); @@ -805,13 +1276,16 @@ mod tests { let listed = list_sources(Some(SourceType::Skill), Some(project), false).unwrap(); assert!(listed.iter().any(|s| s.name == "my-skill" && !s.global)); - let updated = update_source( + let updated = update_source_with_roots( SourceType::Skill, created.path.as_str(), "my-skill", "now does a different thing", "step three", - Some(HashMap::new()), + UpdateSourceOptions { + properties: Some(HashMap::new()), + additional_roots: &[], + }, ) .unwrap(); assert_eq!(updated.description, "now does a different thing"); @@ -946,13 +1420,16 @@ mod tests { ) .unwrap(); - let updated = update_source( + let updated = update_source_with_roots( SourceType::Skill, claude_skill_dir.to_str().unwrap(), "portable", "updated description", "updated body", - Some(HashMap::new()), + UpdateSourceOptions { + properties: Some(HashMap::new()), + additional_roots: &[], + }, ) .unwrap(); @@ -1001,13 +1478,16 @@ mod tests { .join(".goose") .join("skills") .join("no-such-skill"); - let err = update_source( + let err = update_source_with_roots( SourceType::Skill, missing_dir.to_str().unwrap(), "no-such-skill", "d", "c", - Some(HashMap::new()), + UpdateSourceOptions { + properties: Some(HashMap::new()), + additional_roots: &[], + }, ) .unwrap_err(); assert!(format!("{:?}", err).contains("not found")); @@ -1109,19 +1589,32 @@ mod tests { .unwrap_err(); assert!(format!("{:?}", err).contains("not supported")); - let err = update_source( + let err = update_source_with_roots( SourceType::BuiltinSkill, "builtin://skills/x", "x", "d", "c", - Some(HashMap::new()), + UpdateSourceOptions { + properties: Some(HashMap::new()), + additional_roots: &[], + }, ) .unwrap_err(); assert!(format!("{:?}", err).contains("not supported")); - let err = update_source(SourceType::Recipe, "x", "x", "d", "c", Some(HashMap::new())) - .unwrap_err(); + let err = update_source_with_roots( + SourceType::Recipe, + "x", + "x", + "d", + "c", + UpdateSourceOptions { + properties: Some(HashMap::new()), + additional_roots: &[], + }, + ) + .unwrap_err(); assert!(format!("{:?}", err).contains("not supported")); let err = delete_source(SourceType::BuiltinSkill, "builtin://skills/x").unwrap_err(); @@ -1173,13 +1666,16 @@ mod tests { .unwrap(); let skill_dir = tmp.path().join(".agents").join("skills").join("my-dir"); - let updated = update_source( + let updated = update_source_with_roots( SourceType::Skill, skill_dir.to_str().unwrap(), "my-dir", "new description", "new body", - Some(HashMap::new()), + UpdateSourceOptions { + properties: Some(HashMap::new()), + additional_roots: &[], + }, ) .unwrap(); // Name is derived from the frontmatter written by create_source @@ -1268,13 +1764,16 @@ mod tests { .unwrap(); let attempted_escape = project.join(".goose").join("escaped"); - let err = update_source( + let err = update_source_with_roots( SourceType::Skill, attempted_escape.to_str().unwrap(), "escaped", "new description", "new content", - Some(HashMap::new()), + UpdateSourceOptions { + properties: Some(HashMap::new()), + additional_roots: &[], + }, ) .unwrap_err(); assert!(format!("{:?}", err).contains("not found")); diff --git a/crates/goose/tests/acp_fixtures/mod.rs b/crates/goose/tests/acp_fixtures/mod.rs index ae8a112d00..01023fdf13 100644 --- a/crates/goose/tests/acp_fixtures/mod.rs +++ b/crates/goose/tests/acp_fixtures/mod.rs @@ -10,7 +10,7 @@ use agent_client_protocol::schema::{ }; use async_trait::async_trait; use fs_err as fs; -use goose::acp::server::{serve, AcpProviderFactory, GooseAcpAgent}; +use goose::acp::server::{serve, AcpProviderFactory, GooseAcpAgent, GooseAcpAgentOptions}; pub use goose::acp::{map_permission_response, PermissionDecision}; use goose::agents::GoosePlatform; use goose::builtin_extension::register_builtin_extensions; @@ -186,15 +186,16 @@ pub async fn spawn_acp_server_in_process( }) }); - let agent = GooseAcpAgent::new( + let agent = GooseAcpAgent::new(GooseAcpAgentOptions { provider_factory, - builtins.to_vec(), - data_root.to_path_buf(), - data_root.to_path_buf(), + builtins: builtins.to_vec(), + data_dir: data_root.to_path_buf(), + config_dir: data_root.to_path_buf(), goose_mode, disable_session_naming, - GoosePlatform::GooseCli, - ) + goose_platform: GoosePlatform::GooseCli, + additional_source_roots: Vec::new(), + }) .await .unwrap(); let agent = Arc::new(agent); diff --git a/ui/goose2/builtin-sources/agents/ralph.md b/ui/goose2/builtin-sources/agents/ralph.md new file mode 100644 index 0000000000..e02b3d09fd --- /dev/null +++ b/ui/goose2/builtin-sources/agents/ralph.md @@ -0,0 +1,34 @@ +--- +name: Ralph +description: Autonomous iteration agent +provider: goose +model: claude-sonnet-4-20250514 +avatar: "data:image/png;base64,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" +--- +You are an autonomous iteration agent implementing the Ralph Loop pattern. Your core behavior is to repeatedly cycle through tasks until every item is objectively complete, using disk-based state rather than conversation memory. + +## Iteration Cycle + +Each time you are invoked, follow this loop: + +1. **Scan environment**: Read the project structure, git history, progress files (e.g. progress.txt, prd.json, TODO files, or any task-tracking artifacts), and prior commit messages to understand the current state. +2. **Select next task**: Identify the highest-priority incomplete item from the task list or PRD. +3. **Execute**: Implement the change — write code, update configs, fix bugs, or whatever the task requires. +4. **Validate**: Run all available feedback loops — type checking, linting, tests, builds — and fix any failures before moving on. +5. **Commit and record**: Commit your changes with a clear message describing what was done and why. Update the task list to mark the item complete. Append learnings, pitfalls, or confirmed patterns to a progress log. +6. **Repeat or signal completion**: If incomplete tasks remain, loop back to step 1. Only signal completion when every task passes objective verification. + +## Key Principles + +- **Disk is your memory**: State lives in files and git history, not in conversation context. Always read current state from disk at the start of each cycle. +- **Fresh context each cycle**: Treat each iteration as if you have no memory of previous rounds. Re-read progress files and git diffs to orient yourself. +- **External verification over self-assessment**: Do not declare completion based on your own judgment alone. Run tests, check linters, verify builds. Completion must be objectively verifiable. +- **Incremental progress**: Each cycle should produce at least one committed, validated change. Small steps compound into large outcomes. +- **Learnings persist**: When you encounter a pitfall or discover a pattern, record it in the progress log so future iterations benefit. + +## Anti-Patterns to Avoid + +- Declaring work complete without running validation +- Skipping the environment scan and relying on assumptions +- Making large uncommitted changes across multiple tasks in a single cycle +- Ignoring test or build failures and moving to the next task diff --git a/ui/goose2/builtin-sources/agents/scout.md b/ui/goose2/builtin-sources/agents/scout.md new file mode 100644 index 0000000000..10ce582ed5 --- /dev/null +++ b/ui/goose2/builtin-sources/agents/scout.md @@ -0,0 +1,52 @@ +--- +name: Scout +description: Research-plan-implement agent +provider: goose +model: claude-sonnet-4-20250514 +avatar: "data:image/png;base64,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" +--- +You are a methodical agent implementing the Research-Plan-Implement (RPI) pattern. You trade speed for clarity, predictability, and correctness by separating work into three distinct phases. Never skip phases or jump ahead to implementation. + +## Phase 1: Research + +Document what exists without judgment. Your goal is to build a complete technical map before making any decisions. + +- **Find files**: Locate all relevant code, configs, tests, and documentation. +- **Analyze code**: Read files thoroughly and document their functionality, data flow, and dependencies. +- **Find patterns**: Identify conventions, similar features, and architectural patterns already in use. + +Output a research document with code references, flow descriptions, and architectural understanding. The rule for this phase: "Document what exists today. No opinions." + +## Phase 2: Plan + +Design the change with explicit decision-making. Read the research document, then: + +- Ask clarifying questions about scope, constraints, and edge cases. +- Present multiple design options when trade-offs exist, with pros and cons for each. +- Produce a phased implementation plan with file paths, code changes, and explicit success criteria for each phase. +- Include verification steps — both automated (tests, type checks, builds) and manual. + +The plan should be detailed enough that someone else could execute it mechanically. It becomes the source of truth for implementation. + +## Phase 3: Implement + +Execute the plan mechanically, phase by phase. + +- Follow the plan exactly — this is not the time for creative decisions. +- Run verification after each phase before moving to the next. +- Update plan checkboxes or status markers as progress occurs. +- If the plan proves wrong or incomplete, stop implementation, update the plan first, then resume. + +## Key Principles + +- **Phases are sequential**: Research before planning, planning before implementation. Each phase happens with focused attention. +- **Plans are living documents**: If implementation reveals a flaw, return to the plan and update it rather than improvising. +- **Verification is mandatory**: Every phase of implementation must pass its success criteria before proceeding. +- **Scope discipline**: If you discover work outside the original scope during research, flag it separately rather than expanding the plan silently. + +## Anti-Patterns to Avoid + +- Jumping to implementation without research or a plan +- Making research documents that include design decisions (that belongs in the plan) +- Improvising during implementation instead of updating the plan +- Skipping verification steps to move faster diff --git a/ui/goose2/builtin-sources/agents/solo.md b/ui/goose2/builtin-sources/agents/solo.md new file mode 100644 index 0000000000..dbba533f56 --- /dev/null +++ b/ui/goose2/builtin-sources/agents/solo.md @@ -0,0 +1,210 @@ +--- +name: Solo +description: Orchestration agent +provider: goose +model: claude-sonnet-4-20250514 +avatar: "data:image/png;base64,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" +--- +You are an orchestration agent that decomposes complex tasks, dispatches them to specialized subagents, and synthesizes the results. You coordinate rather than build — delegating research, implementation, and review to subagents while you manage the overall plan and integrate their outputs. + +Your job is to **decompose, dispatch, and synthesize** — not to do the work yourself. Subagents get fresh context windows; yours only shrinks. Every tool call you spend producing is context you can't spend coordinating. + +**Hard rule:** If it produces an artifact (code, research, documents, reviews), a subagent produces it. You read, plan, decide, and integrate. The one exception is a truly trivial single-step action — a single command, a quick file read, a one-liner. Everything else gets delegated. + +--- + +## Core Principles + +### 1. Context Is Your Scarcest Resource + +Subagents are disposable — you are not. They get fresh windows; you accumulate state. Guard your context ruthlessly: + +- **Yours:** Reading, planning, deciding, integrating results, resolving conflicts. +- **Theirs:** Researching, building, writing, reviewing. Anything that produces output. + +### 2. Subagents Have Zero Shared Context + +Every subagent starts cold. They know **only** what you put in their instructions. They cannot see your conversation history, other subagents' outputs, or each other. + +This means: +- **Instructions must be self-contained.** Include all relevant context, file paths, requirements, and constraints. Don't reference "the thing we discussed" — they weren't there. +- **Specifics, not pointers.** Don't say "follow the usual pattern." Say exactly what the pattern is. +- **Just enough to decide well.** Not everything you know — just what they need to act correctly. + +### 3. Subagents Cannot Coordinate With Each Other + +They can't wait on each other, share results, or negotiate file ownership. **Overlap is your mistake, not theirs.** You must partition work so no two subagents touch the same files or produce conflicting outputs. + +- **Read-only tasks** (research, review): Safe to overlap and parallelize freely. +- **Write tasks** (code, documents): Must touch strictly separate files. No exceptions. + +### 4. The Hierarchy Is Flat + +One orchestrator, many subagents. Subagents do not spawn their own subagents. You are the only coordinator. + +--- + +## The Workflow + +### Phase 1: Research Before Action + +Every non-trivial task starts with research, not building. Surprises are cheaper on paper than in code. + +1. Spawn 2–5 research subagents in parallel, each covering a different angle (existing code, documentation, external best practices, prior decisions, etc.) +2. Process results as they arrive — don't wait for all of them. +3. Spawn follow-ups where gaps appear. +4. Cancel redundant work once a question is answered. + +**When to stop researching:** Key questions answered, multiple sources agree, and you can explain **why** — not just what. 80% confidence across 2+ sources is enough. Perfect information doesn't exist; waiting for it is a form of procrastination. + +### Phase 2: Plan and Decompose + +Research findings become a plan. The plan becomes 2–5 independent tasks, each: + +- **Self-contained** — can be picked up with no prior context. +- **File-partitioned** — no two tasks touch the same file. +- **Has clear acceptance criteria** — the subagent knows what "done" looks like. + +If the plan itself is a substantial document, delegate writing it too. + +### Phase 3: Dispatch Workers + +Give subagents **context and goals**, not line-by-line scripts. They're agents, not functions. + +**Critical rule: Require incremental output.** Always instruct subagents to write their output files as they go — not all at the end. A subagent that writes incrementally survives cancellation or timeout. A subagent that saves everything for a final write loses it all. + +### Phase 4: Review + +Don't review work yourself — you're biased toward your own plan. Spawn review subagents with specific criteria: + +- Correctness, security, edge cases, style consistency. +- Give reviewers the code/document AND the requirements it should satisfy. +- If the work is important, use **two reviewers** (ideally different models) — agreement is signal; disagreement is data. + +Both must approve before work ships. If either requests changes, fix and re-review. + +### Phase 5: Synthesize + +This is the part only you can do. Subagents produce pieces; you build the whole: + +- Integrate worker outputs into a coherent result. +- Resolve conflicts between reviewers. +- Surface unresolved questions to the user. +- Summarize what was done, what was decided, and what's still open. + +--- + +## Parallel Execution Patterns + +### Fire-and-Forget (Independent Tasks) +Spawn all at once. Collect results. No coordination needed. +Use for: Multiple research angles, independent file changes, parallel reviews. + +### Sequential (Dependent Tasks) +Wait for one to finish before spawning the next. +Use for: "Research first, then implement based on findings." + +### Fan-Out / Fan-In +Spawn many in parallel, then synthesize all results into one output. +Use for: Research → Plan. Multiple workers → Integration review. + +--- + +## Subagent Lifecycle Management + +### Timeouts +Set mental time budgets. Research: 5–10 min. Workers: 10–15 min. Proceed with partial results rather than stall. Incomplete data beats no data. + +### Cancellation +Cancel only when: +1. **Output is no longer needed** — another subagent already answered the question. +2. **They're genuinely stuck** — no progress for an extended period. + +Long-running ≠ stuck. A subagent that's still working is still producing context you can't rebuild. + +### Graceful Degradation +3 of 4 completing is enough. One outlier among agreement gets noted, not obeyed. A subagent that returns empty isn't broken — it just means that angle didn't yield results. + +### Limits +- Max ~8–10 subagents per task. More than that and you lose track. +- Max 2 levels of follow-up spawning (research → follow-up research, not research → follow-up → follow-up → ...). +- Infinite delegation chains don't terminate; you do. + +--- + +## Communicating Subagent Activity + +**This is critical.** You MUST keep the user informed about what your subagents are doing. Every time you spawn or collect a subagent, say so in the channel. Be chatty about it — users want to see the work happening. + +When you **spawn** a subagent, post a message like: +> "🚀 Kicking off a research subagent to look into X..." +> "🔨 Dispatching a worker to implement Y in `path/to/file.rs`..." +> "🔍 Sending this to a reviewer to check for Z..." + +When a subagent **finishes**, post what happened: +> "✅ Research subagent came back — found that X uses pattern Y. Key takeaway: ..." +> "✅ Worker finished implementing the new endpoint. Files changed: ..." +> "⚠️ Reviewer flagged 2 issues: ... Spinning up a fix." + +When things go wrong: +> "❌ Subagent timed out on X. Moving forward with what we have." +> "🔄 Reviewer requested changes — dispatching a new worker with the feedback." + +This gives the user visibility into the orchestration process. Don't just go silent while subagents are running. + +--- + +## Polling and the Reactive Loop + +Spawning subagents isn't the end of the job — it's barely the beginning. You need to stay in a tight processing loop, checking for completed work and acting on it. + +### The Basic Loop +while subagents are still running: + check for completed subagents + + for each completed subagent: + - Read and integrate their output + - Enough to proceed? → Stop waiting, cancel stragglers + - Gaps remain? → Spawn targeted follow-ups + - Pending work now redundant? → Cancel it + + if no completions yet: + sleep 20-30 seconds, then check again + +### Never Sleep Without Thinking First + +Every sleep cycle is a decision point. Before you wait, ask: +1. Is anything already done? Process it before sleeping. +2. Do I already have enough? Three researchers agreed — do I really need the fourth? +3. Has the situation changed? Early results might make pending work obsolete. +4. Is anyone stuck? No progress across multiple cycles = consider cancelling. + +--- + +## When Things Go Sideways + +| Situation | Response | +|-----------|----------| +| Subagents contradict each other | Don't pick the most confident one. Surface the contradiction. Spawn a tie-breaker or escalate to the user. | +| A worker produces something wrong | Don't patch it yourself. Spawn a new worker with corrected instructions + the reviewer's findings. Fresh context beats accumulated confusion. | +| Running low on context | Say so. Summarize state, open items, and what the next session needs. A clean handoff beats a heroic finish. | +| The user goes quiet | Keep working on reversible steps — research, planning, drafts. Pause before anything destructive. | + +--- + +## Anti-Patterns + +❌ **Doing the work yourself.** Every line you write is context you can't spend coordinating. +❌ **Vague instructions.** "Look into auth" → subagent flails. Be specific about what to search, where, and what format to report in. +❌ **Overlapping file writes.** Two subagents editing the same file = corruption. Always partition. +❌ **Waiting for perfection.** 80% from three sources beats 100% from one source that takes forever. +❌ **Reviewing your own plan.** You can't see your own blind spots. That's what reviewers are for. +❌ **Spawning without a plan.** Subagents are cheap but not free. Know what you need before you ask for it. +❌ **Forgetting to integrate.** Raw subagent output is not a deliverable. Synthesis is your job. +❌ **Going silent.** If subagents are working, the user should know about it. Narrate the process. + +--- + +## The One-Sentence Version + +**You are the brain; subagents are the hands. Think, decompose, dispatch, integrate. Never build what you can delegate, and never delegate what requires the full picture.** diff --git a/ui/goose2/src-tauri/Cargo.lock b/ui/goose2/src-tauri/Cargo.lock index f3ee8a4b0e..65cf9897fd 100644 --- a/ui/goose2/src-tauri/Cargo.lock +++ b/ui/goose2/src-tauri/Cargo.lock @@ -600,10 +600,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link 0.2.1", ] @@ -1656,7 +1654,6 @@ name = "goose2" version = "0.1.0" dependencies = [ "base64 0.22.1", - "chrono", "dirs", "doctor", "ignore", @@ -1665,7 +1662,6 @@ dependencies = [ "mime_guess", "serde", "serde_json", - "serde_yaml", "tauri", "tauri-build", "tauri-plugin-app-test-driver", @@ -3702,12 +3698,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "same-file" version = "1.0.6" @@ -4008,19 +3998,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.13.1", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -5262,12 +5239,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "url" version = "2.5.8" diff --git a/ui/goose2/src-tauri/Cargo.toml b/ui/goose2/src-tauri/Cargo.toml index 233b8112a5..c0816e82c2 100644 --- a/ui/goose2/src-tauri/Cargo.toml +++ b/ui/goose2/src-tauri/Cargo.toml @@ -29,8 +29,6 @@ dirs = "6.0.0" log = "0.4.29" tokio = { version = "1.50.0", features = ["full"] } uuid = { version = "1", features = ["v4", "serde"] } -chrono = { version = "0.4", features = ["serde"] } -serde_yaml = "0.9" doctor = { git = "https://github.com/block/builderbot", rev = "8e1c3ec145edc0df5f04b4427cfd758378036862" } ignore = "0.4.25" base64 = "0.22" diff --git a/ui/goose2/src-tauri/src/commands/agents.rs b/ui/goose2/src-tauri/src/commands/agents.rs index 3d3564c4cb..fe52cb5940 100644 --- a/ui/goose2/src-tauri/src/commands/agents.rs +++ b/ui/goose2/src-tauri/src/commands/agents.rs @@ -1,64 +1,5 @@ -use crate::services::personas::PersonaStore; -use crate::types::agents::*; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::path::PathBuf; -use tauri::State; - -#[tauri::command] -pub fn list_personas(store: State<'_, PersonaStore>) -> Vec { - store.list() -} - -#[tauri::command] -pub fn create_persona( - store: State<'_, PersonaStore>, - request: CreatePersonaRequest, -) -> Result { - store.create(request) -} - -#[tauri::command] -pub fn update_persona( - store: State<'_, PersonaStore>, - id: String, - request: UpdatePersonaRequest, -) -> Result { - store.update(&id, request) -} - -#[tauri::command] -pub fn delete_persona(store: State<'_, PersonaStore>, id: String) -> Result<(), String> { - store.delete(&id) -} - -#[tauri::command] -pub fn refresh_personas(store: State<'_, PersonaStore>) -> Vec { - store.refresh_markdown() -} - -/// Save avatar from a local file path for a persona. -/// Copies the file into ~/.goose/avatars/{persona_id}.{ext}. -/// Returns the stored filename (e.g. "persona-id.png"). -#[tauri::command] -pub fn save_persona_avatar(persona_id: String, source_path: String) -> Result { - PersonaStore::save_avatar_from_path(&persona_id, &source_path) -} - -/// Save avatar from raw bytes (for drag-and-drop from the browser). -#[tauri::command] -pub fn save_persona_avatar_bytes( - persona_id: String, - bytes: Vec, - extension: String, -) -> Result { - PersonaStore::save_avatar_from_bytes(&persona_id, &bytes, &extension) -} - -/// Returns the absolute path to the avatars directory (~/.goose/avatars/). -#[tauri::command] -pub fn get_avatars_dir() -> String { - PersonaStore::avatars_dir().to_string_lossy().to_string() -} #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -111,156 +52,6 @@ pub fn read_import_persona_file(source_path: String) -> Result, - #[serde(skip_serializing_if = "Option::is_none")] - provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - model: Option, -} - -/// Result returned by export_persona containing the JSON string and a suggested filename. -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ExportResult { - pub json: String, - pub suggested_filename: String, -} - -/// Convert a display name into a filesystem-safe slug. -/// Lowercase, replace non-alphanumeric with hyphens, collapse runs, trim, max 50 chars. -pub fn slugify(name: &str) -> String { - let slug: String = name - .to_lowercase() - .chars() - .map(|c| if c.is_alphanumeric() { c } else { '-' }) - .collect(); - - // Collapse consecutive hyphens - let mut collapsed = String::with_capacity(slug.len()); - let mut prev_hyphen = false; - for c in slug.chars() { - if c == '-' { - if !prev_hyphen { - collapsed.push('-'); - } - prev_hyphen = true; - } else { - collapsed.push(c); - prev_hyphen = false; - } - } - - let trimmed = collapsed.trim_matches('-'); - let result = if trimmed.len() > 50 { - // Cut at 50 chars without splitting mid-char, then trim trailing hyphens - trimmed[..50].trim_end_matches('-').to_string() - } else { - trimmed.to_string() - }; - - if result.is_empty() { - "persona".to_string() - } else { - result - } -} - -/// Export a persona as sprout-compatible JSON (version 1). -/// Returns the JSON string and a suggested filename. -#[tauri::command] -pub fn export_persona(store: State<'_, PersonaStore>, id: String) -> Result { - let persona = store - .get(&id) - .ok_or_else(|| format!("Persona '{}' not found", id))?; - - // For export, only include URL avatars (local files aren't portable) - let export_avatar = match &persona.avatar { - Some(Avatar::Url(url)) => Some(Avatar::Url(url.clone())), - _ => None, - }; - - let export = PersonaExportV1 { - version: 1, - display_name: persona.display_name.clone(), - system_prompt: persona.system_prompt, - avatar: export_avatar, - provider: persona.provider, - model: persona.model, - }; - - let json = serde_json::to_string_pretty(&export) - .map_err(|e| format!("Failed to serialize persona: {}", e))?; - - let slug = slugify(&persona.display_name); - let suggested_filename = format!("{}.persona.json", slug); - - Ok(ExportResult { - json, - suggested_filename, - }) -} - -/// Import personas from sprout-compatible JSON (version 1). -/// Accepts raw file bytes and the original filename. -/// Returns the list of newly created personas. -#[tauri::command] -pub fn import_personas( - store: State<'_, PersonaStore>, - file_bytes: Vec, - file_name: String, -) -> Result, String> { - // Validate file extension - if !file_name.ends_with(".persona.json") && !file_name.ends_with(".json") { - return Err("Unsupported file type. Expected a .persona.json or .json file.".to_string()); - } - - // Parse the bytes as UTF-8 - let content = - String::from_utf8(file_bytes).map_err(|_| "File is not valid UTF-8 text".to_string())?; - - // Parse as JSON - let export: PersonaExportV1 = - serde_json::from_str(&content).map_err(|e| format!("Invalid persona JSON: {}", e))?; - - // Validate version - if export.version != 1 { - return Err(format!( - "Unsupported persona format version {}. Expected version 1.", - export.version - )); - } - - // Validate required fields - if export.display_name.trim().is_empty() { - return Err("Persona displayName cannot be empty".to_string()); - } - if export.system_prompt.trim().is_empty() { - return Err("Persona systemPrompt cannot be empty".to_string()); - } - - // Create the persona via the store - let request = CreatePersonaRequest { - display_name: export.display_name, - avatar: export.avatar, - system_prompt: export.system_prompt, - provider: export.provider, - model: export.model, - }; - - let persona = store.create(request)?; - Ok(vec![persona]) -} - #[cfg(test)] mod tests { use super::validate_import_persona_path; diff --git a/ui/goose2/src-tauri/src/lib.rs b/ui/goose2/src-tauri/src/lib.rs index 193b180700..6664f1517d 100644 --- a/ui/goose2/src-tauri/src/lib.rs +++ b/ui/goose2/src-tauri/src/lib.rs @@ -3,7 +3,6 @@ mod services; mod types; use services::distro_bundle::DistroBundleState; -use services::personas::PersonaStore; use tauri::Manager; use tauri_plugin_window_state::StateFlags; @@ -25,8 +24,7 @@ pub fn run() { tauri_plugin_window_state::Builder::default() .with_state_flags(StateFlags::all() & !StateFlags::VISIBLE) .build(), - ) - .manage(PersonaStore::new()); + ); #[cfg(feature = "app-test-driver")] let builder = builder.plugin(tauri_plugin_app_test_driver::init()); @@ -37,17 +35,7 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ - commands::agents::list_personas, - commands::agents::create_persona, - commands::agents::update_persona, - commands::agents::delete_persona, - commands::agents::refresh_personas, - commands::agents::export_persona, - commands::agents::import_personas, commands::agents::read_import_persona_file, - commands::agents::save_persona_avatar, - commands::agents::save_persona_avatar_bytes, - commands::agents::get_avatars_dir, commands::acp::get_goose_serve_url, commands::acp::get_goose_serve_host_info, commands::project_icons::scan_project_icons, diff --git a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs index 62e1043f74..f06abf8892 100644 --- a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs +++ b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs @@ -1,4 +1,4 @@ -use tauri::Manager; +use tauri::{Manager, Runtime}; use tauri_plugin_shell::ShellExt; use std::ffi::OsString; @@ -13,6 +13,8 @@ use tokio::sync::OnceCell; const GOOSE_SERVE_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); const GOOSE_SERVE_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(100); const LOCALHOST: &str = "127.0.0.1"; +const ADDITIONAL_AGENT_SOURCE_ROOTS_ENV: &str = "ADDITIONAL_AGENT_SOURCE_ROOTS"; +const BUNDLED_AGENT_ROOT_DIR: &str = "builtin-sources/agents"; // --------------------------------------------------------------------------- // GooseServeProcess — singleton that owns the long-lived `goose serve` child // --------------------------------------------------------------------------- @@ -84,8 +86,10 @@ impl GooseServeProcess { } } + command.arg("serve"); + add_bundled_agent_root_env(&app_handle, &mut command); + command - .arg("serve") .arg("--host") .arg(LOCALHOST) .arg("--port") @@ -121,6 +125,46 @@ impl GooseServeProcess { } } +fn add_bundled_agent_root_env(manager: &impl Manager, command: &mut Command) { + let resource_dir = match manager.path().resource_dir() { + Ok(path) => path, + Err(error) => { + log::warn!("Failed to resolve Tauri resource dir for bundled sources: {error}"); + return; + } + }; + + let root = resource_dir.join(BUNDLED_AGENT_ROOT_DIR); + if !root.is_dir() { + log::debug!( + "No bundled source root found at {}; skipping", + root.display() + ); + return; + } + + append_additional_agent_roots_env(command, &root); +} + +fn append_additional_agent_roots_env(command: &mut Command, root: &std::path::Path) { + let existing = std::env::var_os(ADDITIONAL_AGENT_SOURCE_ROOTS_ENV); + let mut roots: Vec = existing + .as_ref() + .map(std::env::split_paths) + .map(Iterator::collect) + .unwrap_or_default(); + roots.push(root.to_path_buf()); + + match std::env::join_paths(&roots) { + Ok(joined) => { + command.env(ADDITIONAL_AGENT_SOURCE_ROOTS_ENV, joined); + } + Err(error) => { + eprintln!("Failed to set {ADDITIONAL_AGENT_SOURCE_ROOTS_ENV}: {error}"); + } + } +} + pub fn get_goose_command(app_handle: &tauri::AppHandle) -> Result { if let Ok(override_path) = std::env::var("GOOSE_BIN") { Ok(Command::new(override_path)) diff --git a/ui/goose2/src-tauri/src/services/mod.rs b/ui/goose2/src-tauri/src/services/mod.rs index d077c56186..121497d660 100644 --- a/ui/goose2/src-tauri/src/services/mod.rs +++ b/ui/goose2/src-tauri/src/services/mod.rs @@ -1,3 +1,2 @@ pub mod acp; pub mod distro_bundle; -pub mod personas; diff --git a/ui/goose2/src-tauri/src/services/personas.rs b/ui/goose2/src-tauri/src/services/personas.rs deleted file mode 100644 index 5a2f0fb487..0000000000 --- a/ui/goose2/src-tauri/src/services/personas.rs +++ /dev/null @@ -1,457 +0,0 @@ -use crate::types::agents::{ - builtin_personas, Avatar, CreatePersonaRequest, Persona, UpdatePersonaRequest, -}; -use log::warn; -use std::collections::HashSet; -use std::path::{Component, Path, PathBuf}; -use std::sync::Mutex; - -pub struct PersonaStore { - personas: Mutex>, - store_path: PathBuf, -} - -/// YAML frontmatter fields parsed from markdown persona files. -#[derive(serde::Deserialize)] -struct MarkdownFrontmatter { - name: String, - description: Option, -} - -impl PersonaStore { - pub fn new() -> Self { - let store_path = Self::store_path(); - let stored = Self::load_from_disk(&store_path); - let markdown = Self::load_markdown_personas(); - let merged = Self::merge_all(stored, markdown); - Self { - personas: Mutex::new(merged), - store_path, - } - } - - fn store_path() -> PathBuf { - let base = dirs::home_dir().expect("home dir"); - base.join(".goose").join("personas.json") - } - - /// Path to the avatars directory (~/.goose/avatars/). - pub fn avatars_dir() -> PathBuf { - dirs::home_dir() - .expect("home dir") - .join(".goose") - .join("avatars") - } - - fn load_from_disk(path: &PathBuf) -> Vec { - match std::fs::read_to_string(path) { - Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(), - Err(_) => Vec::new(), - } - } - - /// Merge builtins, JSON custom personas, and markdown personas. - /// Priority: builtins first, then JSON custom, then markdown. - /// Deduplication is by display_name (case-insensitive). - fn merge_all(stored: Vec, markdown: Vec) -> Vec { - let builtins = builtin_personas(); - - let mut result = builtins; - let mut seen_names: HashSet = result - .iter() - .map(|p| p.display_name.to_lowercase()) - .collect(); - let mut seen_ids: HashSet = result.iter().map(|p| p.id.clone()).collect(); - - // Add custom (non-builtin) personas from JSON - for persona in stored { - if !seen_ids.contains(&persona.id) { - seen_names.insert(persona.display_name.to_lowercase()); - seen_ids.insert(persona.id.clone()); - result.push(persona); - } - } - - // Add markdown personas, skipping any whose name already exists - for persona in markdown { - if !seen_names.contains(&persona.display_name.to_lowercase()) - && !seen_ids.contains(&persona.id) - { - seen_names.insert(persona.display_name.to_lowercase()); - seen_ids.insert(persona.id.clone()); - result.push(persona); - } - } - - result - } - - /// Directory containing markdown persona files. - fn agents_dir() -> PathBuf { - dirs::home_dir() - .expect("home dir") - .join(".goose") - .join("agents") - } - - /// Scan `~/.goose/agents/*.md` and parse each into a Persona. - fn load_markdown_personas() -> Vec { - let dir = Self::agents_dir(); - if !dir.is_dir() { - return Vec::new(); - } - - let mut personas = Vec::new(); - - let entries = match std::fs::read_dir(&dir) { - Ok(e) => e, - Err(err) => { - warn!("Failed to read agents directory {:?}: {}", dir, err); - return Vec::new(); - } - }; - - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) != Some("md") { - continue; - } - - match Self::parse_markdown_persona(&path) { - Ok(persona) => personas.push(persona), - Err(err) => { - warn!("Skipping {:?}: {}", path, err); - } - } - } - - personas - } - - /// Parse a single markdown file with YAML frontmatter into a Persona. - fn parse_markdown_persona(path: &std::path::Path) -> Result { - let content = - std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?; - - // Expect file to start with "---" - let trimmed = content.trim_start(); - if !trimmed.starts_with("---") { - return Err("Missing frontmatter delimiter".to_string()); - } - - // Find the closing "---" - let after_first = &trimmed[3..]; - let end_idx = after_first - .find("\n---") - .ok_or_else(|| "Missing closing frontmatter delimiter".to_string())?; - - let yaml_str = &after_first[..end_idx]; - let body = after_first[end_idx + 4..].trim().to_string(); - - let frontmatter: MarkdownFrontmatter = serde_yaml::from_str(yaml_str) - .map_err(|e| format!("Invalid frontmatter YAML: {}", e))?; - - // Derive a stable ID from the filename (without extension) - let slug = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown") - .to_string(); - let id = format!("md-{}", slug); - - // Use the file modification time for timestamps, fall back to now - let mod_time = std::fs::metadata(path) - .and_then(|m| m.modified()) - .ok() - .and_then(|t| { - let duration = t.duration_since(std::time::UNIX_EPOCH).ok()?; - let dt = chrono::DateTime::from_timestamp( - duration.as_secs() as i64, - duration.subsec_nanos(), - )?; - Some(dt.to_rfc3339()) - }) - .unwrap_or_else(|| chrono::Utc::now().to_rfc3339()); - - // Use the body as system prompt. If body is empty, use description or a fallback. - let system_prompt = if body.is_empty() { - frontmatter - .description - .clone() - .unwrap_or_else(|| format!("You are {}.", frontmatter.name)) - } else { - body - }; - - Ok(Persona { - id, - display_name: frontmatter.name, - avatar: None, - system_prompt, - provider: None, - model: None, - is_builtin: false, - is_from_disk: true, - created_at: mod_time.clone(), - updated_at: mod_time, - }) - } - - fn markdown_persona_path(id: &str) -> Result { - let slug = id - .strip_prefix("md-") - .ok_or_else(|| format!("Persona '{}' is not a file-backed persona", id))?; - Self::validate_markdown_persona_slug(slug)?; - Ok(Self::agents_dir().join(format!("{}.md", slug))) - } - - fn validate_markdown_persona_slug(slug: &str) -> Result<(), String> { - if slug.chars().any(|c| matches!(c, '/' | '\\')) { - return Err(format!("Persona '{}' has an invalid file-backed ID", slug)); - } - - let mut components = Path::new(slug).components(); - match (components.next(), components.next()) { - (Some(Component::Normal(_)), None) => Ok(()), - _ => Err(format!("Persona '{}' has an invalid file-backed ID", slug)), - } - } - - /// Re-scan markdown personas and update the in-memory list. - /// Returns the full updated persona list. - pub fn refresh_markdown(&self) -> Vec { - let stored = Self::load_from_disk(&self.store_path); - let markdown = Self::load_markdown_personas(); - let merged = Self::merge_all(stored, markdown); - - let mut personas = self.personas.lock().unwrap(); - *personas = merged; - personas.clone() - } - - fn save_to_disk(&self, personas: &[Persona]) { - if let Some(parent) = self.store_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - // Only persist custom personas (not builtins, not from markdown files) - let custom: Vec<&Persona> = personas - .iter() - .filter(|p| !p.is_builtin && !p.is_from_disk) - .collect(); - if let Ok(json) = serde_json::to_string_pretty(&custom) { - let _ = std::fs::write(&self.store_path, json); - } - } - - pub fn list(&self) -> Vec { - let personas = self.personas.lock().unwrap(); - personas.clone() - } - - #[allow(dead_code)] - pub fn get(&self, id: &str) -> Option { - let personas = self.personas.lock().unwrap(); - personas.iter().find(|p| p.id == id).cloned() - } - - pub fn create(&self, req: CreatePersonaRequest) -> Result { - let now = chrono::Utc::now().to_rfc3339(); - let persona = Persona { - id: uuid::Uuid::new_v4().to_string(), - display_name: req.display_name, - avatar: req.avatar, - system_prompt: req.system_prompt, - provider: req.provider, - model: req.model, - is_builtin: false, - is_from_disk: false, - created_at: now.clone(), - updated_at: now, - }; - - let mut personas = self.personas.lock().unwrap(); - personas.push(persona.clone()); - self.save_to_disk(&personas); - Ok(persona) - } - - pub fn update(&self, id: &str, req: UpdatePersonaRequest) -> Result { - let mut personas = self.personas.lock().unwrap(); - let persona = personas - .iter_mut() - .find(|p| p.id == id) - .ok_or_else(|| format!("Persona '{}' not found", id))?; - - if persona.is_builtin { - return Err("Cannot update a built-in persona".to_string()); - } - if persona.is_from_disk { - return Err("Cannot update a markdown persona — edit the file directly".to_string()); - } - - if let Some(name) = req.display_name { - persona.display_name = name; - } - if let Some(avatar_value) = req.avatar { - // Some(None) → clear, Some(Some(a)) → set - persona.avatar = avatar_value; - } - if let Some(prompt) = req.system_prompt { - persona.system_prompt = prompt; - } - if let Some(provider) = req.provider { - persona.provider = Some(provider); - } - if let Some(model) = req.model { - persona.model = Some(model); - } - persona.updated_at = chrono::Utc::now().to_rfc3339(); - - let updated = persona.clone(); - self.save_to_disk(&personas); - Ok(updated) - } - - pub fn delete(&self, id: &str) -> Result<(), String> { - let mut personas = self.personas.lock().unwrap(); - - let persona = personas - .iter() - .find(|p| p.id == id) - .cloned() - .ok_or_else(|| format!("Persona '{}' not found", id))?; - - if persona.is_builtin { - return Err("Cannot delete a built-in persona".to_string()); - } - if persona.is_from_disk { - let path = Self::markdown_persona_path(id)?; - match std::fs::remove_file(&path) { - Ok(_) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => { - return Err(format!( - "Failed to delete file-backed persona '{}': {}", - path.display(), - err - )); - } - } - - personas.retain(|p| p.id != id); - self.save_to_disk(&personas); - return Ok(()); - } - - // Clean up local avatar file if present - if let Some(Avatar::Local(filename)) = &persona.avatar { - let path = Self::avatars_dir().join(filename); - let _ = std::fs::remove_file(path); - } - - personas.retain(|p| p.id != id); - self.save_to_disk(&personas); - Ok(()) - } - - /// Copy an avatar image from a source path to ~/.goose/avatars/{persona_id}.{ext}. - /// Returns the filename (not full path). - pub fn save_avatar_from_path(persona_id: &str, source_path: &str) -> Result { - let avatars_dir = Self::avatars_dir(); - std::fs::create_dir_all(&avatars_dir) - .map_err(|e| format!("Failed to create avatars directory: {}", e))?; - - let source = std::path::Path::new(source_path); - - // Extract extension from source filename - let ext = source - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("png") - .to_lowercase(); - - let stored_name = format!("{}.{}", persona_id, ext); - let dest = avatars_dir.join(&stored_name); - - // Remove any existing avatar for this persona (different extension) - if let Ok(entries) = std::fs::read_dir(&avatars_dir) { - let prefix = format!("{}.", persona_id); - for entry in entries.flatten() { - let name = entry.file_name(); - if let Some(name_str) = name.to_str() { - if name_str.starts_with(&prefix) && name_str != stored_name { - let _ = std::fs::remove_file(entry.path()); - } - } - } - } - - std::fs::copy(source, &dest).map_err(|e| format!("Failed to copy avatar file: {}", e))?; - - Ok(stored_name) - } - - /// Write avatar image bytes to ~/.goose/avatars/{persona_id}.{ext}. - /// Returns the filename (not full path). - pub fn save_avatar_from_bytes( - persona_id: &str, - bytes: &[u8], - extension: &str, - ) -> Result { - let avatars_dir = Self::avatars_dir(); - std::fs::create_dir_all(&avatars_dir) - .map_err(|e| format!("Failed to create avatars directory: {}", e))?; - - let ext = extension.to_lowercase(); - let stored_name = format!("{}.{}", persona_id, ext); - let dest = avatars_dir.join(&stored_name); - - // Remove any existing avatar for this persona (different extension) - if let Ok(entries) = std::fs::read_dir(&avatars_dir) { - let prefix = format!("{}.", persona_id); - for entry in entries.flatten() { - let name = entry.file_name(); - if let Some(name_str) = name.to_str() { - if name_str.starts_with(&prefix) && name_str != stored_name { - let _ = std::fs::remove_file(entry.path()); - } - } - } - } - - std::fs::write(&dest, bytes).map_err(|e| format!("Failed to write avatar file: {}", e))?; - - Ok(stored_name) - } - - /// Delete avatar file for a persona. - #[allow(dead_code)] - pub fn delete_avatar_file(filename: &str) { - let path = Self::avatars_dir().join(filename); - let _ = std::fs::remove_file(path); - } -} - -#[cfg(test)] -mod tests { - use super::PersonaStore; - - #[test] - fn markdown_persona_path_rejects_parent_segments() { - assert!(PersonaStore::markdown_persona_path("md-../secret").is_err()); - assert!(PersonaStore::markdown_persona_path("md-..").is_err()); - } - - #[test] - fn markdown_persona_path_rejects_path_separators() { - assert!(PersonaStore::markdown_persona_path("md-nested/slug").is_err()); - assert!(PersonaStore::markdown_persona_path(r"md-nested\slug").is_err()); - } - - #[test] - fn markdown_persona_path_accepts_normal_slug() { - let path = PersonaStore::markdown_persona_path("md-scout").unwrap(); - let file_name = path.file_name().and_then(|name| name.to_str()); - assert_eq!(file_name, Some("scout.md")); - } -} diff --git a/ui/goose2/src-tauri/src/types/agents.rs b/ui/goose2/src-tauri/src/types/agents.rs deleted file mode 100644 index a083a0686c..0000000000 --- a/ui/goose2/src-tauri/src/types/agents.rs +++ /dev/null @@ -1,198 +0,0 @@ -use serde::{Deserialize, Deserializer, Serialize}; - -/// Avatar for a persona — either a remote URL or a local file in ~/.goose/avatars/. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", content = "value")] -pub enum Avatar { - #[serde(rename = "url")] - Url(String), - #[serde(rename = "local")] - Local(String), -} - -/// Custom deserializer that handles migration from old format. -/// Accepts: -/// - null → None -/// - "https://..." (bare string) → Some(Avatar::Url(s)) -/// - { "type": "url", "value": "..." } → Some(Avatar::Url(...)) -/// - { "type": "local", "value": "x" } → Some(Avatar::Local(...)) -fn deserialize_avatar_compat<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum AvatarOrString { - Avatar(Avatar), - BareString(String), - } - - let opt: Option = Option::deserialize(deserializer)?; - match opt { - None => Ok(None), - Some(AvatarOrString::BareString(s)) => { - if s.is_empty() { - Ok(None) - } else { - Ok(Some(Avatar::Url(s))) - } - } - Some(AvatarOrString::Avatar(a)) => Ok(Some(a)), - } -} - -/// Deserializer for UpdatePersonaRequest: distinguishes "field absent" from "field: null". -/// - JSON field absent → None (don't update) -/// - "avatar": null → Some(None) (clear the avatar) -/// - "avatar": {...} or "str" → Some(Some(Avatar)) -fn deserialize_avatar_update<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum AvatarOrString { - Avatar(Avatar), - BareString(String), - } - - let opt: Option = Option::deserialize(deserializer)?; - match opt { - None => Ok(Some(None)), // explicit null → clear - Some(AvatarOrString::BareString(s)) => { - if s.is_empty() { - Ok(Some(None)) - } else { - Ok(Some(Some(Avatar::Url(s)))) - } - } - Some(AvatarOrString::Avatar(a)) => Ok(Some(Some(a))), - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Persona { - pub id: String, - pub display_name: String, - #[serde( - default, - skip_serializing_if = "Option::is_none", - alias = "avatarUrl", - deserialize_with = "deserialize_avatar_compat" - )] - pub avatar: Option, - pub system_prompt: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, - pub is_builtin: bool, - #[serde(default)] - pub is_from_disk: bool, - pub created_at: String, - pub updated_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreatePersonaRequest { - pub display_name: String, - #[serde( - default, - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_avatar_compat" - )] - pub avatar: Option, - pub system_prompt: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdatePersonaRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub display_name: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_avatar_update" - )] - pub avatar: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, -} - -#[allow(dead_code)] -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Agent { - pub id: String, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub persona_id: Option, - pub provider: String, - pub model: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, - pub connection_type: String, - pub status: String, - pub is_builtin: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub acp_endpoint: Option, - pub created_at: String, - pub updated_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Session { - pub id: String, - pub title: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub agent_id: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub project_id: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub provider_id: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub persona_id: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub model_name: Option, - pub created_at: String, - pub updated_at: String, - pub message_count: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_message_preview: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub archived_at: Option, -} - -/// Partial update for a session — only provided fields are applied. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionUpdate { - pub title: Option, - pub provider_id: Option, - pub persona_id: Option, - pub model_name: Option, - #[serde(default, deserialize_with = "deserialize_nullable_field")] - pub project_id: Option>, -} - -fn deserialize_nullable_field<'de, D, T>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, - T: Deserialize<'de>, -{ - Option::>::deserialize(deserializer) -} - -pub use super::builtin_personas::builtin_personas; diff --git a/ui/goose2/src-tauri/src/types/builtin_personas.rs b/ui/goose2/src-tauri/src/types/builtin_personas.rs deleted file mode 100644 index 27513d9e7e..0000000000 --- a/ui/goose2/src-tauri/src/types/builtin_personas.rs +++ /dev/null @@ -1,326 +0,0 @@ -use super::agents::{Avatar, Persona}; - -const SOLO_AVATAR_DATA: &str = "data:image/png;base64,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"; -const SCOUT_AVATAR_DATA: &str = "data:image/png;base64,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"; -const RALPH_AVATAR_DATA: &str = "data:image/png;base64,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"; - -const SOLO_SYSTEM_PROMPT_TEXT: &str = r#"You are an orchestration agent that decomposes complex tasks, dispatches them to specialized subagents, and synthesizes the results. You coordinate rather than build — delegating research, implementation, and review to subagents while you manage the overall plan and integrate their outputs. - -Your job is to **decompose, dispatch, and synthesize** — not to do the work yourself. Subagents get fresh context windows; yours only shrinks. Every tool call you spend producing is context you can't spend coordinating. - -**Hard rule:** If it produces an artifact (code, research, documents, reviews), a subagent produces it. You read, plan, decide, and integrate. The one exception is a truly trivial single-step action — a single command, a quick file read, a one-liner. Everything else gets delegated. - ---- - -## Core Principles - -### 1. Context Is Your Scarcest Resource - -Subagents are disposable — you are not. They get fresh windows; you accumulate state. Guard your context ruthlessly: - -- **Yours:** Reading, planning, deciding, integrating results, resolving conflicts. -- **Theirs:** Researching, building, writing, reviewing. Anything that produces output. - -### 2. Subagents Have Zero Shared Context - -Every subagent starts cold. They know **only** what you put in their instructions. They cannot see your conversation history, other subagents' outputs, or each other. - -This means: -- **Instructions must be self-contained.** Include all relevant context, file paths, requirements, and constraints. Don't reference "the thing we discussed" — they weren't there. -- **Specifics, not pointers.** Don't say "follow the usual pattern." Say exactly what the pattern is. -- **Just enough to decide well.** Not everything you know — just what they need to act correctly. - -### 3. Subagents Cannot Coordinate With Each Other - -They can't wait on each other, share results, or negotiate file ownership. **Overlap is your mistake, not theirs.** You must partition work so no two subagents touch the same files or produce conflicting outputs. - -- **Read-only tasks** (research, review): Safe to overlap and parallelize freely. -- **Write tasks** (code, documents): Must touch strictly separate files. No exceptions. - -### 4. The Hierarchy Is Flat - -One orchestrator, many subagents. Subagents do not spawn their own subagents. You are the only coordinator. - ---- - -## The Workflow - -### Phase 1: Research Before Action - -Every non-trivial task starts with research, not building. Surprises are cheaper on paper than in code. - -1. Spawn 2–5 research subagents in parallel, each covering a different angle (existing code, documentation, external best practices, prior decisions, etc.) -2. Process results as they arrive — don't wait for all of them. -3. Spawn follow-ups where gaps appear. -4. Cancel redundant work once a question is answered. - -**When to stop researching:** Key questions answered, multiple sources agree, and you can explain **why** — not just what. 80% confidence across 2+ sources is enough. Perfect information doesn't exist; waiting for it is a form of procrastination. - -### Phase 2: Plan and Decompose - -Research findings become a plan. The plan becomes 2–5 independent tasks, each: - -- **Self-contained** — can be picked up with no prior context. -- **File-partitioned** — no two tasks touch the same file. -- **Has clear acceptance criteria** — the subagent knows what "done" looks like. - -If the plan itself is a substantial document, delegate writing it too. - -### Phase 3: Dispatch Workers - -Give subagents **context and goals**, not line-by-line scripts. They're agents, not functions. - -**Critical rule: Require incremental output.** Always instruct subagents to write their output files as they go — not all at the end. A subagent that writes incrementally survives cancellation or timeout. A subagent that saves everything for a final write loses it all. - -### Phase 4: Review - -Don't review work yourself — you're biased toward your own plan. Spawn review subagents with specific criteria: - -- Correctness, security, edge cases, style consistency. -- Give reviewers the code/document AND the requirements it should satisfy. -- If the work is important, use **two reviewers** (ideally different models) — agreement is signal; disagreement is data. - -Both must approve before work ships. If either requests changes, fix and re-review. - -### Phase 5: Synthesize - -This is the part only you can do. Subagents produce pieces; you build the whole: - -- Integrate worker outputs into a coherent result. -- Resolve conflicts between reviewers. -- Surface unresolved questions to the user. -- Summarize what was done, what was decided, and what's still open. - ---- - -## Parallel Execution Patterns - -### Fire-and-Forget (Independent Tasks) -Spawn all at once. Collect results. No coordination needed. -Use for: Multiple research angles, independent file changes, parallel reviews. - -### Sequential (Dependent Tasks) -Wait for one to finish before spawning the next. -Use for: "Research first, then implement based on findings." - -### Fan-Out / Fan-In -Spawn many in parallel, then synthesize all results into one output. -Use for: Research → Plan. Multiple workers → Integration review. - ---- - -## Subagent Lifecycle Management - -### Timeouts -Set mental time budgets. Research: 5–10 min. Workers: 10–15 min. Proceed with partial results rather than stall. Incomplete data beats no data. - -### Cancellation -Cancel only when: -1. **Output is no longer needed** — another subagent already answered the question. -2. **They're genuinely stuck** — no progress for an extended period. - -Long-running ≠ stuck. A subagent that's still working is still producing context you can't rebuild. - -### Graceful Degradation -3 of 4 completing is enough. One outlier among agreement gets noted, not obeyed. A subagent that returns empty isn't broken — it just means that angle didn't yield results. - -### Limits -- Max ~8–10 subagents per task. More than that and you lose track. -- Max 2 levels of follow-up spawning (research → follow-up research, not research → follow-up → follow-up → ...). -- Infinite delegation chains don't terminate; you do. - ---- - -## Communicating Subagent Activity - -**This is critical.** You MUST keep the user informed about what your subagents are doing. Every time you spawn or collect a subagent, say so in the channel. Be chatty about it — users want to see the work happening. - -When you **spawn** a subagent, post a message like: -> "🚀 Kicking off a research subagent to look into X..." -> "🔨 Dispatching a worker to implement Y in `path/to/file.rs`..." -> "🔍 Sending this to a reviewer to check for Z..." - -When a subagent **finishes**, post what happened: -> "✅ Research subagent came back — found that X uses pattern Y. Key takeaway: ..." -> "✅ Worker finished implementing the new endpoint. Files changed: ..." -> "⚠️ Reviewer flagged 2 issues: ... Spinning up a fix." - -When things go wrong: -> "❌ Subagent timed out on X. Moving forward with what we have." -> "🔄 Reviewer requested changes — dispatching a new worker with the feedback." - -This gives the user visibility into the orchestration process. Don't just go silent while subagents are running. - ---- - -## Polling and the Reactive Loop - -Spawning subagents isn't the end of the job — it's barely the beginning. You need to stay in a tight processing loop, checking for completed work and acting on it. - -### The Basic Loop -while subagents are still running: - check for completed subagents - - for each completed subagent: - - Read and integrate their output - - Enough to proceed? → Stop waiting, cancel stragglers - - Gaps remain? → Spawn targeted follow-ups - - Pending work now redundant? → Cancel it - - if no completions yet: - sleep 20-30 seconds, then check again - -### Never Sleep Without Thinking First - -Every sleep cycle is a decision point. Before you wait, ask: -1. Is anything already done? Process it before sleeping. -2. Do I already have enough? Three researchers agreed — do I really need the fourth? -3. Has the situation changed? Early results might make pending work obsolete. -4. Is anyone stuck? No progress across multiple cycles = consider cancelling. - ---- - -## When Things Go Sideways - -| Situation | Response | -|-----------|----------| -| Subagents contradict each other | Don't pick the most confident one. Surface the contradiction. Spawn a tie-breaker or escalate to the user. | -| A worker produces something wrong | Don't patch it yourself. Spawn a new worker with corrected instructions + the reviewer's findings. Fresh context beats accumulated confusion. | -| Running low on context | Say so. Summarize state, open items, and what the next session needs. A clean handoff beats a heroic finish. | -| The user goes quiet | Keep working on reversible steps — research, planning, drafts. Pause before anything destructive. | - ---- - -## Anti-Patterns - -❌ **Doing the work yourself.** Every line you write is context you can't spend coordinating. -❌ **Vague instructions.** "Look into auth" → subagent flails. Be specific about what to search, where, and what format to report in. -❌ **Overlapping file writes.** Two subagents editing the same file = corruption. Always partition. -❌ **Waiting for perfection.** 80% from three sources beats 100% from one source that takes forever. -❌ **Reviewing your own plan.** You can't see your own blind spots. That's what reviewers are for. -❌ **Spawning without a plan.** Subagents are cheap but not free. Know what you need before you ask for it. -❌ **Forgetting to integrate.** Raw subagent output is not a deliverable. Synthesis is your job. -❌ **Going silent.** If subagents are working, the user should know about it. Narrate the process. - ---- - -## The One-Sentence Version - -**You are the brain; subagents are the hands. Think, decompose, dispatch, integrate. Never build what you can delegate, and never delegate what requires the full picture.**"#; - -const SCOUT_SYSTEM_PROMPT_TEXT: &str = r#"You are a methodical agent implementing the Research-Plan-Implement (RPI) pattern. You trade speed for clarity, predictability, and correctness by separating work into three distinct phases. Never skip phases or jump ahead to implementation. - -## Phase 1: Research - -Document what exists without judgment. Your goal is to build a complete technical map before making any decisions. - -- **Find files**: Locate all relevant code, configs, tests, and documentation. -- **Analyze code**: Read files thoroughly and document their functionality, data flow, and dependencies. -- **Find patterns**: Identify conventions, similar features, and architectural patterns already in use. - -Output a research document with code references, flow descriptions, and architectural understanding. The rule for this phase: "Document what exists today. No opinions." - -## Phase 2: Plan - -Design the change with explicit decision-making. Read the research document, then: - -- Ask clarifying questions about scope, constraints, and edge cases. -- Present multiple design options when trade-offs exist, with pros and cons for each. -- Produce a phased implementation plan with file paths, code changes, and explicit success criteria for each phase. -- Include verification steps — both automated (tests, type checks, builds) and manual. - -The plan should be detailed enough that someone else could execute it mechanically. It becomes the source of truth for implementation. - -## Phase 3: Implement - -Execute the plan mechanically, phase by phase. - -- Follow the plan exactly — this is not the time for creative decisions. -- Run verification after each phase before moving to the next. -- Update plan checkboxes or status markers as progress occurs. -- If the plan proves wrong or incomplete, stop implementation, update the plan first, then resume. - -## Key Principles - -- **Phases are sequential**: Research before planning, planning before implementation. Each phase happens with focused attention. -- **Plans are living documents**: If implementation reveals a flaw, return to the plan and update it rather than improvising. -- **Verification is mandatory**: Every phase of implementation must pass its success criteria before proceeding. -- **Scope discipline**: If you discover work outside the original scope during research, flag it separately rather than expanding the plan silently. - -## Anti-Patterns to Avoid - -- Jumping to implementation without research or a plan -- Making research documents that include design decisions (that belongs in the plan) -- Improvising during implementation instead of updating the plan -- Skipping verification steps to move faster"#; - -const RALPH_SYSTEM_PROMPT_TEXT: &str = r#"You are an autonomous iteration agent implementing the Ralph Loop pattern. Your core behavior is to repeatedly cycle through tasks until every item is objectively complete, using disk-based state rather than conversation memory. - -## Iteration Cycle - -Each time you are invoked, follow this loop: - -1. **Scan environment**: Read the project structure, git history, progress files (e.g. progress.txt, prd.json, TODO files, or any task-tracking artifacts), and prior commit messages to understand the current state. -2. **Select next task**: Identify the highest-priority incomplete item from the task list or PRD. -3. **Execute**: Implement the change — write code, update configs, fix bugs, or whatever the task requires. -4. **Validate**: Run all available feedback loops — type checking, linting, tests, builds — and fix any failures before moving on. -5. **Commit and record**: Commit your changes with a clear message describing what was done and why. Update the task list to mark the item complete. Append learnings, pitfalls, or confirmed patterns to a progress log. -6. **Repeat or signal completion**: If incomplete tasks remain, loop back to step 1. Only signal completion when every task passes objective verification. - -## Key Principles - -- **Disk is your memory**: State lives in files and git history, not in conversation context. Always read current state from disk at the start of each cycle. -- **Fresh context each cycle**: Treat each iteration as if you have no memory of previous rounds. Re-read progress files and git diffs to orient yourself. -- **External verification over self-assessment**: Do not declare completion based on your own judgment alone. Run tests, check linters, verify builds. Completion must be objectively verifiable. -- **Incremental progress**: Each cycle should produce at least one committed, validated change. Small steps compound into large outcomes. -- **Learnings persist**: When you encounter a pitfall or discover a pattern, record it in the progress log so future iterations benefit. - -## Anti-Patterns to Avoid - -- Declaring work complete without running validation -- Skipping the environment scan and relying on assumptions -- Making large uncommitted changes across multiple tasks in a single cycle -- Ignoring test or build failures and moving to the next task"#; - -/// Built-in persona definitions -pub fn builtin_personas() -> Vec { - let now = chrono::Utc::now().to_rfc3339(); - vec![ - Persona { - id: "builtin-solo".to_string(), - display_name: "Solo".to_string(), - avatar: Some(Avatar::Url(SOLO_AVATAR_DATA.to_string())), - system_prompt: SOLO_SYSTEM_PROMPT_TEXT.to_string(), - provider: Some("goose".to_string()), - model: Some("claude-sonnet-4-20250514".to_string()), - is_builtin: true, - is_from_disk: false, - created_at: now.clone(), - updated_at: now.clone(), - }, - Persona { - id: "builtin-scout".to_string(), - display_name: "Scout".to_string(), - avatar: Some(Avatar::Url(SCOUT_AVATAR_DATA.to_string())), - system_prompt: SCOUT_SYSTEM_PROMPT_TEXT.to_string(), - provider: Some("goose".to_string()), - model: Some("claude-sonnet-4-20250514".to_string()), - is_builtin: true, - is_from_disk: false, - created_at: now.clone(), - updated_at: now.clone(), - }, - Persona { - id: "builtin-ralph".to_string(), - display_name: "Ralph".to_string(), - avatar: Some(Avatar::Url(RALPH_AVATAR_DATA.to_string())), - system_prompt: RALPH_SYSTEM_PROMPT_TEXT.to_string(), - provider: Some("goose".to_string()), - model: Some("claude-sonnet-4-20250514".to_string()), - is_builtin: true, - is_from_disk: false, - created_at: now.clone(), - updated_at: now, - }, - ] -} diff --git a/ui/goose2/src-tauri/src/types/mod.rs b/ui/goose2/src-tauri/src/types/mod.rs index a49df73eee..0f2bf8288e 100644 --- a/ui/goose2/src-tauri/src/types/mod.rs +++ b/ui/goose2/src-tauri/src/types/mod.rs @@ -1,5 +1,2 @@ #[allow(dead_code)] -pub mod agents; -pub mod builtin_personas; -#[allow(dead_code)] pub mod messages; diff --git a/ui/goose2/src-tauri/tauri.conf.json b/ui/goose2/src-tauri/tauri.conf.json index a5abbfd893..4c45e0204b 100644 --- a/ui/goose2/src-tauri/tauri.conf.json +++ b/ui/goose2/src-tauri/tauri.conf.json @@ -47,7 +47,8 @@ "icons/icon.ico" ], "resources": { - "../distro": "distro" + "../distro": "distro", + "../builtin-sources": "builtin-sources" }, "externalBin": ["../../../target/release/goose"], "linux": { diff --git a/ui/goose2/src/features/agents/lib/personaPresentation.ts b/ui/goose2/src/features/agents/lib/personaPresentation.ts index ee44c3585b..2b51774eb1 100644 --- a/ui/goose2/src/features/agents/lib/personaPresentation.ts +++ b/ui/goose2/src/features/agents/lib/personaPresentation.ts @@ -6,6 +6,9 @@ export function getPersonaSource(persona: Persona): PersonaSource { if (persona.isBuiltin) { return "builtin"; } + if (persona.writable === true) { + return "custom"; + } if (persona.isFromDisk) { return "file"; } @@ -13,5 +16,5 @@ export function getPersonaSource(persona: Persona): PersonaSource { } export function isPersonaReadOnly(persona: Persona): boolean { - return getPersonaSource(persona) !== "custom"; + return persona.writable === false || getPersonaSource(persona) !== "custom"; } diff --git a/ui/goose2/src/features/agents/stores/agentStore.ts b/ui/goose2/src/features/agents/stores/agentStore.ts index 7efee9df40..72b8cedb69 100644 --- a/ui/goose2/src/features/agents/stores/agentStore.ts +++ b/ui/goose2/src/features/agents/stores/agentStore.ts @@ -219,5 +219,6 @@ export const useAgentStore = create((set, get) => ({ getBuiltinPersonas: () => get().personas.filter((p) => p.isBuiltin), - getCustomPersonas: () => get().personas.filter((p) => !p.isBuiltin), + getCustomPersonas: () => + get().personas.filter((p) => !p.isBuiltin && p.writable !== false), })); diff --git a/ui/goose2/src/features/agents/ui/AgentsView.tsx b/ui/goose2/src/features/agents/ui/AgentsView.tsx index 23abe723a2..7cac120ab0 100644 --- a/ui/goose2/src/features/agents/ui/AgentsView.tsx +++ b/ui/goose2/src/features/agents/ui/AgentsView.tsx @@ -119,7 +119,8 @@ export function AgentsView() { ); const handleDeletePersona = useCallback((persona: Persona) => { - if (getPersonaSource(persona) === "builtin") return; + if (getPersonaSource(persona) === "builtin" || persona.writable === false) + return; setDeletingPersona(persona); }, []); diff --git a/ui/goose2/src/features/agents/ui/AvatarDropZone.tsx b/ui/goose2/src/features/agents/ui/AvatarDropZone.tsx index 9ff982adce..a8c3c8a690 100644 --- a/ui/goose2/src/features/agents/ui/AvatarDropZone.tsx +++ b/ui/goose2/src/features/agents/ui/AvatarDropZone.tsx @@ -4,21 +4,27 @@ import { Camera, X } from "lucide-react"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { useAvatarSrc } from "@/shared/hooks/useAvatarSrc"; -import { savePersonaAvatar, savePersonaAvatarBytes } from "@/shared/api/agents"; import { open } from "@tauri-apps/plugin-dialog"; import type { Avatar } from "@/shared/types/agents"; const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "svg"]; +function filePathToFileUrl(filePath: string): string { + const normalizedPath = filePath.replaceAll("\\", "/"); + const url = new URL("file://"); + url.pathname = normalizedPath.startsWith("/") + ? normalizedPath + : `/${normalizedPath}`; + return url.href; +} + interface AvatarDropZoneProps { - personaId: string; avatar: Avatar | null | undefined; onChange: (avatar: Avatar | null) => void; disabled?: boolean; } export function AvatarDropZone({ - personaId, avatar, onChange, disabled = false, @@ -44,11 +50,20 @@ export function AvatarDropZone({ setIsUploading(true); try { - const buffer = await file.arrayBuffer(); - const bytes = Array.from(new Uint8Array(buffer)); - const filename = await savePersonaAvatarBytes(personaId, bytes, ext); + const value = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener("load", () => { + if (typeof reader.result === "string") { + resolve(reader.result); + } else { + reject(new Error("Avatar file could not be read as a data URL")); + } + }); + reader.addEventListener("error", () => reject(reader.error)); + reader.readAsDataURL(file); + }); - onChange({ type: "local", value: filename }); + onChange({ type: "url", value }); } catch (err) { console.error("Failed to save avatar:", err); setError(t("avatar.saveFailed")); @@ -56,7 +71,7 @@ export function AvatarDropZone({ setIsUploading(false); } }, - [personaId, onChange, t], + [onChange, t], ); /** Save a file selected via the native file picker (has a path). */ @@ -72,9 +87,7 @@ export function AvatarDropZone({ setIsUploading(true); try { - const filename = await savePersonaAvatar(personaId, filePath); - - onChange({ type: "local", value: filename }); + onChange({ type: "url", value: filePathToFileUrl(filePath) }); } catch (err) { console.error("Failed to save avatar:", err); setError(t("avatar.saveFailed")); @@ -82,7 +95,7 @@ export function AvatarDropZone({ setIsUploading(false); } }, - [personaId, onChange, t], + [onChange, t], ); // Standard HTML5 drag-and-drop (works when dragDropEnabled is false) diff --git a/ui/goose2/src/features/agents/ui/PersonaCard.tsx b/ui/goose2/src/features/agents/ui/PersonaCard.tsx index 83e2c9cb5f..ef8cdf5a8d 100644 --- a/ui/goose2/src/features/agents/ui/PersonaCard.tsx +++ b/ui/goose2/src/features/agents/ui/PersonaCard.tsx @@ -13,7 +13,10 @@ import { } from "@/shared/ui/dropdown-menu"; import { useAvatarSrc } from "@/shared/hooks/useAvatarSrc"; import type { Persona } from "@/shared/types/agents"; -import { getPersonaSource } from "@/features/agents/lib/personaPresentation"; +import { + getPersonaSource, + isPersonaReadOnly, +} from "@/features/agents/lib/personaPresentation"; interface PersonaCardProps { persona: Persona; @@ -40,8 +43,9 @@ export function PersonaCard({ const initials = persona.displayName.charAt(0).toUpperCase(); const avatarSrc = useAvatarSrc(persona.avatar); const personaSource = getPersonaSource(persona); - const canEditPersona = personaSource === "custom"; - const canDeletePersona = personaSource !== "builtin"; + const canEditPersona = !isPersonaReadOnly(persona); + const canDeletePersona = + personaSource !== "builtin" && persona.writable !== false; const providerModelLabel = [persona.provider, persona.model] .filter(Boolean) .join(" / "); diff --git a/ui/goose2/src/features/agents/ui/PersonaEditor.tsx b/ui/goose2/src/features/agents/ui/PersonaEditor.tsx index 7655f6afe5..013a43c476 100644 --- a/ui/goose2/src/features/agents/ui/PersonaEditor.tsx +++ b/ui/goose2/src/features/agents/ui/PersonaEditor.tsx @@ -72,8 +72,9 @@ export function PersonaEditor({ const readOnlyBySource = persona ? isPersonaReadOnly(persona) : false; const isReadOnly = detailsMode || readOnlyBySource; const personaSource = persona ? getPersonaSource(persona) : "custom"; - const canEditPersona = personaSource === "custom"; - const canDeletePersona = personaSource !== "builtin"; + const canEditPersona = !readOnlyBySource; + const canDeletePersona = + personaSource !== "builtin" && persona?.writable !== false; const acpProviders = useAgentStore((s) => s.providers); const setProviders = useAgentStore((s) => s.setProviders); const mergeInventoryEntries = useProviderInventoryStore( @@ -187,9 +188,6 @@ export function PersonaEditor({ const initials = displayName.charAt(0).toUpperCase() || "?"; - // For new personas, use a temporary ID for the avatar upload - const avatarPersonaId = persona?.id ?? "new-persona"; - return ( !open && onClose()}> @@ -236,7 +234,6 @@ export function PersonaEditor({ ) : ( { const builtins = personas - .filter((p) => p.isBuiltin) + .filter((p) => p.isBuiltin || p.writable === false) .sort((a, b) => a.displayName.localeCompare(b.displayName)); const custom = personas - .filter((p) => !p.isBuiltin) + .filter((p) => !p.isBuiltin && p.writable !== false) .sort((a, b) => a.displayName.localeCompare(b.displayName)); return [...builtins, ...custom]; }, [personas]); diff --git a/ui/goose2/src/features/chat/ui/MentionAutocomplete.tsx b/ui/goose2/src/features/chat/ui/MentionAutocomplete.tsx index 23ff5db0b2..9fb0e6c691 100644 --- a/ui/goose2/src/features/chat/ui/MentionAutocomplete.tsx +++ b/ui/goose2/src/features/chat/ui/MentionAutocomplete.tsx @@ -246,12 +246,12 @@ function MentionAvatar({ persona }: { persona: Persona }) {
- {persona.isBuiltin ? ( + {persona.isBuiltin || persona.writable === false ? ( ) : ( diff --git a/ui/goose2/src/features/chat/ui/PersonaPicker.tsx b/ui/goose2/src/features/chat/ui/PersonaPicker.tsx index 6435f0a40d..09675f8d89 100644 --- a/ui/goose2/src/features/chat/ui/PersonaPicker.tsx +++ b/ui/goose2/src/features/chat/ui/PersonaPicker.tsx @@ -43,11 +43,11 @@ export function PersonaPicker({ ); const builtinPersonas = useMemo( - () => personas.filter((p) => p.isBuiltin), + () => personas.filter((p) => p.isBuiltin || p.writable === false), [personas], ); const customPersonas = useMemo( - () => personas.filter((p) => !p.isBuiltin), + () => personas.filter((p) => !p.isBuiltin && p.writable !== false), [personas], ); @@ -211,7 +211,9 @@ function PersonaAvatar({ ); } - const isBuiltin = persona?.isBuiltin ?? true; + const isBuiltin = persona + ? persona.isBuiltin || persona.writable === false + : true; return (
({ - invoke: vi.fn(), +const mockGooseSourcesCreate = vi.fn(); +const mockGooseSourcesDelete = vi.fn(); +const mockGooseSourcesExport = vi.fn(); +const mockGooseSourcesImport = vi.fn(); +const mockGooseSourcesList = vi.fn(); +const mockGooseSourcesUpdate = vi.fn(); + +vi.mock("@/shared/api/acpConnection", () => ({ + getClient: async () => ({ + goose: { + GooseSourcesCreate: (...args: unknown[]) => + mockGooseSourcesCreate(...args), + GooseSourcesDelete: (...args: unknown[]) => + mockGooseSourcesDelete(...args), + GooseSourcesExport: (...args: unknown[]) => + mockGooseSourcesExport(...args), + GooseSourcesImport: (...args: unknown[]) => + mockGooseSourcesImport(...args), + GooseSourcesList: (...args: unknown[]) => mockGooseSourcesList(...args), + GooseSourcesUpdate: (...args: unknown[]) => + mockGooseSourcesUpdate(...args), + }, + }), })); -const mockedInvoke = vi.mocked(invoke); +const source = { + type: "agent", + name: "Scout", + description: "Agent", + content: "Research carefully.", + path: "/Users/test/.agents/agents/scout.md", + global: true, + writable: true, + properties: { + provider: "goose", + model: "claude-sonnet-4", + avatar: "file:///Users/test/.goose/avatars/agents/scout.png", + }, +}; describe("agents API", () => { beforeEach(() => { vi.clearAllMocks(); }); - // ── exportPersona ──────────────────────────────────────────────────── + it("listPersonas maps agent sources to personas", async () => { + mockGooseSourcesList.mockResolvedValue({ sources: [source] }); - it("exportPersona invokes correct Tauri command with ID", async () => { - const mockResult = { - json: '{"displayName":"Test"}', - suggestedFilename: "test.json", - }; - mockedInvoke.mockResolvedValue(mockResult); + const result = await listPersonas(); - const result = await exportPersona("persona-123"); - - expect(mockedInvoke).toHaveBeenCalledWith("export_persona", { - id: "persona-123", - }); - expect(result).toEqual(mockResult); + expect(mockGooseSourcesList).toHaveBeenCalledWith({ type: "agent" }); + expect(result).toEqual([ + { + id: source.path, + displayName: "Scout", + avatar: { type: "url", value: source.properties.avatar }, + systemPrompt: "Research carefully.", + provider: "goose", + model: "claude-sonnet-4", + isBuiltin: false, + isFromDisk: true, + writable: true, + createdAt: "", + updatedAt: "", + }, + ]); }); - // ── importPersonas ─────────────────────────────────────────────────── + it("createPersona creates a global agent source", async () => { + mockGooseSourcesCreate.mockResolvedValue({ source }); - it("importPersonas invokes correct Tauri command with bytes and filename", async () => { - const mockPersonas = [ - { - id: "imported-1", - displayName: "Imported", - systemPrompt: "Hello", - isBuiltin: false, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", - }, - ]; - mockedInvoke.mockResolvedValue(mockPersonas); - - const fileBytes = [0x7b, 0x7d]; // "{}" - const result = await importPersonas(fileBytes, "personas.json"); - - expect(mockedInvoke).toHaveBeenCalledWith("import_personas", { - fileBytes, - fileName: "personas.json", + const result = await createPersona({ + displayName: "Scout", + avatar: { type: "url", value: source.properties.avatar }, + systemPrompt: "Research carefully.", + provider: "goose", + model: "claude-sonnet-4", }); - expect(result).toEqual(mockPersonas); + + expect(mockGooseSourcesCreate).toHaveBeenCalledWith({ + type: "agent", + name: "Scout", + description: "Agent", + content: "Research carefully.", + properties: { + provider: "goose", + model: "claude-sonnet-4", + avatar: source.properties.avatar, + }, + global: true, + }); + expect(result.displayName).toBe("Scout"); }); - // ── refreshPersonas ────────────────────────────────────────────────── + it("updatePersona loads existing agent source and updates by path", async () => { + mockGooseSourcesList.mockResolvedValue({ sources: [source] }); + mockGooseSourcesUpdate.mockResolvedValue({ + source: { ...source, name: "Scout 2" }, + }); - it("refreshPersonas invokes correct Tauri command", async () => { - const mockPersonas = [ - { - id: "p1", - displayName: "Refreshed", - systemPrompt: "Prompt", - isBuiltin: false, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", + const result = await updatePersona(source.path, { + displayName: "Scout 2", + }); + + expect(mockGooseSourcesUpdate).toHaveBeenCalledWith({ + type: "agent", + path: source.path, + name: "Scout 2", + description: "Agent", + content: "Research carefully.", + properties: { + provider: "goose", + model: "claude-sonnet-4", + avatar: source.properties.avatar, }, - ]; - mockedInvoke.mockResolvedValue(mockPersonas); + }); + expect(result.displayName).toBe("Scout 2"); + }); + + it("deletePersona deletes an agent source by path", async () => { + mockGooseSourcesDelete.mockResolvedValue(undefined); + + await deletePersona(source.path); + + expect(mockGooseSourcesDelete).toHaveBeenCalledWith({ + type: "agent", + path: source.path, + }); + }); + + it("exportPersona exports an agent source", async () => { + mockGooseSourcesExport.mockResolvedValue({ + json: '{"type":"agent"}', + filename: "scout.agent.json", + }); + + const result = await exportPersona(source.path); + + expect(mockGooseSourcesExport).toHaveBeenCalledWith({ + type: "agent", + path: source.path, + }); + expect(result).toEqual({ + json: '{"type":"agent"}', + suggestedFilename: "scout.agent.json", + }); + }); + + it("importPersonas imports agent source JSON", async () => { + mockGooseSourcesImport.mockResolvedValue({ sources: [source] }); + const data = JSON.stringify({ + version: 1, + type: "agent", + name: "Scout", + description: "Agent", + content: "Research carefully.", + }); + const fileBytes = Array.from(new TextEncoder().encode(data)); + + const result = await importPersonas(fileBytes, "scout.agent.json"); + + expect(mockGooseSourcesImport).toHaveBeenCalledWith({ + data, + global: true, + }); + expect(result).toHaveLength(1); + }); + + it("refreshPersonas lists personas", async () => { + mockGooseSourcesList.mockResolvedValue({ sources: [source] }); const result = await refreshPersonas(); - expect(mockedInvoke).toHaveBeenCalledWith("refresh_personas"); - expect(result).toEqual(mockPersonas); + expect(mockGooseSourcesList).toHaveBeenCalledWith({ type: "agent" }); + expect(result).toHaveLength(1); }); }); diff --git a/ui/goose2/src/shared/api/agents.ts b/ui/goose2/src/shared/api/agents.ts index 570002d6aa..5d22eeeeb8 100644 --- a/ui/goose2/src/shared/api/agents.ts +++ b/ui/goose2/src/shared/api/agents.ts @@ -1,33 +1,155 @@ import { invoke } from "@tauri-apps/api/core"; +import type { SourceEntry } from "@aaif/goose-sdk"; +import { getClient } from "@/shared/api/acpConnection"; import type { Persona, CreatePersonaRequest, UpdatePersonaRequest, + Avatar, } from "@/shared/types/agents"; +const AGENT_SOURCE_TYPE = "agent" as const; +const AGENT_DESCRIPTION = "Agent"; + +type AgentSourceProperties = { + provider?: string; + model?: string; + avatar?: string; +}; + +type AgentSourceEntry = SourceEntry & { + type: typeof AGENT_SOURCE_TYPE; + properties: AgentSourceProperties; +}; + +function isAgentSource(source: SourceEntry): source is AgentSourceEntry { + return source.type === AGENT_SOURCE_TYPE; +} + +function avatarToProperty( + avatar: Avatar | null | undefined, +): string | undefined { + if (!avatar) return undefined; + return avatar.value; +} + +function propertyToAvatar(value: string | undefined): Avatar | null { + if (!value) return null; + return { type: "url", value }; +} + +function personaProperties( + request: CreatePersonaRequest | UpdatePersonaRequest, +): AgentSourceProperties | undefined { + const properties: AgentSourceProperties = {}; + if (request.provider) properties.provider = request.provider; + if (request.model) properties.model = request.model; + const avatar = avatarToProperty(request.avatar); + if (avatar) properties.avatar = avatar; + return properties; +} + +function toPersona(source: AgentSourceEntry): Persona { + const writable = source.writable !== false; + return { + id: source.path, + displayName: source.name, + avatar: propertyToAvatar(source.properties?.avatar), + systemPrompt: source.content, + provider: source.properties?.provider, + model: source.properties?.model, + isBuiltin: !writable, + isFromDisk: writable, + writable, + createdAt: "", + updatedAt: "", + }; +} + +async function listAgentSources(): Promise { + const client = await getClient(); + const response = await client.goose.GooseSourcesList({ + type: AGENT_SOURCE_TYPE, + }); + return response.sources.filter(isAgentSource); +} + +async function getAgentSource(id: string): Promise { + const source = (await listAgentSources()).find( + (source) => source.path === id, + ); + if (!source) { + throw new Error(`Agent '${id}' not found`); + } + return source; +} + export async function listPersonas(): Promise { - return invoke("list_personas"); + return (await listAgentSources()).map(toPersona); } export async function createPersona( request: CreatePersonaRequest, ): Promise { - return invoke("create_persona", { request }); + const client = await getClient(); + const response = await client.goose.GooseSourcesCreate({ + type: AGENT_SOURCE_TYPE, + name: request.displayName, + description: AGENT_DESCRIPTION, + content: request.systemPrompt, + properties: personaProperties(request), + global: true, + }); + + if (!isAgentSource(response.source)) { + throw new Error(`Unexpected source type returned: ${response.source.type}`); + } + + return toPersona(response.source); } export async function updatePersona( id: string, request: UpdatePersonaRequest, ): Promise { - return invoke("update_persona", { id, request }); + const existing = await getAgentSource(id); + const client = await getClient(); + const merged: CreatePersonaRequest = { + displayName: request.displayName ?? existing.name, + avatar: + request.avatar === undefined + ? propertyToAvatar(existing.properties?.avatar) + : request.avatar, + systemPrompt: request.systemPrompt ?? existing.content, + provider: request.provider ?? existing.properties?.provider, + model: request.model ?? existing.properties?.model, + }; + const response = await client.goose.GooseSourcesUpdate({ + type: AGENT_SOURCE_TYPE, + path: id, + name: merged.displayName, + description: existing.description || AGENT_DESCRIPTION, + content: merged.systemPrompt, + properties: personaProperties(merged), + }); + + if (!isAgentSource(response.source)) { + throw new Error(`Unexpected source type returned: ${response.source.type}`); + } + + return toPersona(response.source); } export async function deletePersona(id: string): Promise { - return invoke("delete_persona", { id }); + const client = await getClient(); + await client.goose.GooseSourcesDelete({ + type: AGENT_SOURCE_TYPE, + path: id, + }); } export async function refreshPersonas(): Promise { - return invoke("refresh_personas"); + return listPersonas(); } export interface ExportResult { @@ -36,14 +158,61 @@ export interface ExportResult { } export async function exportPersona(id: string): Promise { - return invoke("export_persona", { id }); + const client = await getClient(); + const response = await client.goose.GooseSourcesExport({ + type: AGENT_SOURCE_TYPE, + path: id, + }); + return { json: response.json, suggestedFilename: response.filename }; } export async function importPersonas( fileBytes: number[], fileName: string, ): Promise { - return invoke("import_personas", { fileBytes, fileName }); + if ( + !fileName.endsWith(".agent.json") && + !fileName.endsWith(".persona.json") && + !fileName.endsWith(".json") + ) { + throw new Error( + "File must have a .agent.json, .persona.json, or .json extension", + ); + } + + const raw = new TextDecoder().decode(new Uint8Array(fileBytes)); + const parsed = JSON.parse(raw) as Record; + const data = + parsed.type === AGENT_SOURCE_TYPE + ? raw + : JSON.stringify({ + version: parsed.version ?? 1, + type: AGENT_SOURCE_TYPE, + name: parsed.displayName ?? parsed.name, + description: AGENT_DESCRIPTION, + content: + parsed.systemPrompt ?? parsed.content ?? parsed.instructions ?? "", + properties: { + provider: parsed.provider, + model: parsed.model, + avatar: + typeof parsed.avatar === "string" + ? parsed.avatar + : typeof parsed.avatar === "object" && + parsed.avatar !== null && + "value" in parsed.avatar + ? (parsed.avatar as { value?: unknown }).value + : undefined, + }, + }); + + const client = await getClient(); + const response = await client.goose.GooseSourcesImport({ + data, + global: true, + }); + + return response.sources.filter(isAgentSource).map(toPersona); } export interface ImportFileReadResult { @@ -56,22 +225,3 @@ export async function readImportPersonaFile( ): Promise { return invoke("read_import_persona_file", { sourcePath }); } - -export async function savePersonaAvatar( - personaId: string, - sourcePath: string, -): Promise { - return invoke("save_persona_avatar", { personaId, sourcePath }); -} - -export async function savePersonaAvatarBytes( - personaId: string, - bytes: number[], - extension: string, -): Promise { - return invoke("save_persona_avatar_bytes", { personaId, bytes, extension }); -} - -export async function getAvatarsDir(): Promise { - return invoke("get_avatars_dir"); -} diff --git a/ui/goose2/src/shared/lib/avatarUrl.ts b/ui/goose2/src/shared/lib/avatarUrl.ts index 5171f2f103..d380b252d8 100644 --- a/ui/goose2/src/shared/lib/avatarUrl.ts +++ b/ui/goose2/src/shared/lib/avatarUrl.ts @@ -1,28 +1,22 @@ import { convertFileSrc } from "@tauri-apps/api/core"; -import { getAvatarsDir } from "@/shared/api/agents"; import type { Avatar } from "@/shared/types/agents"; -let cachedAvatarsDir: string | null = null; - -async function ensureAvatarsDir(): Promise { - if (!cachedAvatarsDir) { - cachedAvatarsDir = await getAvatarsDir(); +function resolveFileUrl(value: string): string { + try { + return convertFileSrc(decodeURIComponent(new URL(value).pathname)); + } catch { + return value; } - return cachedAvatarsDir; } -/** - * Resolve an Avatar to a displayable image URL. - * Lazily fetches the avatars directory on first call for a local avatar. - */ export async function resolveAvatarSrc( avatar: Avatar | null | undefined, ): Promise { if (!avatar) return undefined; - if (avatar.type === "url") return avatar.value; - if (avatar.type === "local") { - const dir = await ensureAvatarsDir(); - return convertFileSrc(`${dir}/${avatar.value}`); + if (avatar.type === "url") { + return avatar.value.startsWith("file://") + ? resolveFileUrl(avatar.value) + : avatar.value; } return undefined; } diff --git a/ui/goose2/src/shared/types/agents.ts b/ui/goose2/src/shared/types/agents.ts index bbcdba5058..e209aac913 100644 --- a/ui/goose2/src/shared/types/agents.ts +++ b/ui/goose2/src/shared/types/agents.ts @@ -3,10 +3,26 @@ // a narrow union. export type ProviderType = string; -// Avatar type — either a remote URL or a local file in ~/.goose/avatars/ -export type Avatar = - | { type: "url"; value: string } - | { type: "local"; value: string }; +export interface ProviderConfig { + type: ProviderType; + name: string; + description?: string; + models: ModelInfo[]; + requiresApiKey: boolean; + apiKeyEnvVar?: string; +} + +export interface ModelInfo { + id: string; + name: string; + contextWindow: number; + supportsTools: boolean; + supportsVision: boolean; + supportsThinking: boolean; +} + +// Avatar type — remote, data, and file URLs are stored directly in source properties. +export type Avatar = { type: "url"; value: string }; // Persona types (from sprout) export interface Persona { @@ -18,6 +34,7 @@ export interface Persona { model?: string; isBuiltin: boolean; isFromDisk?: boolean; + writable?: boolean; createdAt: string; updatedAt: string; } diff --git a/ui/goose2/tests/e2e/fixtures/tauri-mock.ts b/ui/goose2/tests/e2e/fixtures/tauri-mock.ts index 108e507574..6ca637d8cf 100644 --- a/ui/goose2/tests/e2e/fixtures/tauri-mock.ts +++ b/ui/goose2/tests/e2e/fixtures/tauri-mock.ts @@ -124,6 +124,22 @@ export function buildInitScript(options?: { supportingFiles: [], }); + const personaToSourceEntry = (p) => ({ + type: "agent", + name: p.displayName ?? p.name, + description: p.description ?? "Agent", + content: p.systemPrompt ?? p.content ?? "", + path: p.id ?? p.path ?? ("/mock/.agents/agents/" + (p.displayName ?? p.name)), + global: p.global ?? true, + writable: p.writable ?? !p.isBuiltin, + supportingFiles: [], + properties: { + provider: p.provider, + model: p.model, + avatar: typeof p.avatar === "string" ? p.avatar : p.avatar?.value, + }, + }); + const projectToSourceEntry = (p) => ({ type: "project", name: p.id ?? p.name?.toLowerCase(), @@ -263,30 +279,42 @@ export function buildInitScript(options?: { return jsonRpcResult(message.id, {}); case "_goose/sources/list": { const sourceType = message.params?.type; + if (sourceType === "agent") { + return jsonRpcResult(message.id, { sources: PERSONAS.map(personaToSourceEntry) }); + } if (sourceType === "project") { return jsonRpcResult(message.id, { sources: PROJECTS.map(projectToSourceEntry) }); } return jsonRpcResult(message.id, { sources: SKILLS.map(skillToSourceEntry) }); } - case "_goose/sources/create": + case "_goose/sources/create": { + const sourceType = message.params?.type ?? "skill"; + const name = message.params?.name ?? (sourceType === "agent" ? "New Agent" : "new-skill"); return jsonRpcResult(message.id, { source: { - name: message.params?.name ?? "new-skill", - type: "skill", + name, + type: sourceType, description: message.params?.description ?? "", content: message.params?.content ?? "", - path: "/mock/.agents/skills/" + (message.params?.name ?? "new-skill"), + path: "/mock/.agents/" + (sourceType === "agent" ? "agents" : "skills") + "/" + name, global: message.params?.global ?? true, + writable: true, + supportingFiles: [], + properties: message.params?.properties ?? {}, }, }); + } case "_goose/sources/update": case "goose/sources/update": { - const path = message.params?.path ?? "/mock/.agents/skills/updated-skill"; + const sourceType = message.params?.type ?? "skill"; + const path = + message.params?.path ?? + (sourceType === "agent" ? "/mock/.agents/agents/updated-agent" : "/mock/.agents/skills/updated-skill"); const nextName = message.params?.name; const name = typeof nextName === "string" && nextName.length > 0 ? nextName - : String(path).split("/").filter(Boolean).at(-1) ?? "updated-skill"; + : String(path).split("/").filter(Boolean).at(-1) ?? (sourceType === "agent" ? "updated-agent" : "updated-skill"); const segments = String(path).split("/").filter(Boolean); if (segments.length > 0) { segments[segments.length - 1] = name; @@ -295,12 +323,14 @@ export function buildInitScript(options?: { return jsonRpcResult(message.id, { source: { name, - type: "skill", + type: sourceType, description: message.params?.description ?? "", content: message.params?.content ?? "", path: updatedPath, global: message.params?.global ?? true, + writable: true, supportingFiles: [], + properties: message.params?.properties ?? {}, }, }); } diff --git a/ui/sdk/src/generated/types.gen.ts b/ui/sdk/src/generated/types.gen.ts index e5c17b6547..8b880d3090 100644 --- a/ui/sdk/src/generated/types.gen.ts +++ b/ui/sdk/src/generated/types.gen.ts @@ -797,6 +797,11 @@ export type SourceEntry = { * when it lives inside a specific project. */ global: boolean; + /** + * True when this source can be modified through source CRUD methods. + * Client-provided bundled sources are returned as read-only. + */ + writable?: boolean; /** * Paths (absolute) of additional files that live alongside the source. * Only skills currently populate this; empty for other source types. diff --git a/ui/sdk/src/generated/zod.gen.ts b/ui/sdk/src/generated/zod.gen.ts index ce8c3339f5..6d72c52acb 100644 --- a/ui/sdk/src/generated/zod.gen.ts +++ b/ui/sdk/src/generated/zod.gen.ts @@ -791,6 +791,7 @@ export const zSourceEntry = z.object({ content: z.string(), path: z.string(), global: z.boolean(), + writable: z.boolean().optional().default(false), supportingFiles: z.array(z.string()).optional(), properties: z.record(z.unknown()).optional() });