diff --git a/src/agent/builtin_profiles.rs b/src/agent/builtin_profiles.rs new file mode 100644 index 0000000..21525f7 --- /dev/null +++ b/src/agent/builtin_profiles.rs @@ -0,0 +1,137 @@ +use crate::agent::profile::{AgentProfile, ProfileLlmConfig}; +use crate::security::SecurityScope; + +/// Built-in "planner" profile for task breakdown and planning +pub fn planner() -> AgentProfile { + AgentProfile { + name: "planner".to_string(), + extends: None, + role: "Task breakdown specialist".to_string(), + system_prompt: "You are a task breakdown specialist. Your role is to analyze high-level goals and break them into concrete, actionable tasks. Each task should have clear acceptance criteria and be assigned to the most appropriate agent type (coder, reviewer, tester, or researcher). Prioritize tasks based on dependencies and criticality.".to_string(), + allowed_tools: vec![ + "file".to_string(), + "shell".to_string(), + "graph".to_string(), + "signal_completion".to_string(), + ], + security: SecurityScope { + allowed_paths: vec!["*".to_string()], + denied_paths: vec![], + allowed_commands: vec!["*".to_string()], + read_only: true, + can_create_files: false, + network_access: false, + }, + llm: ProfileLlmConfig::default(), + turn_limit: Some(100), + token_budget: Some(200_000), + } +} + +/// Built-in "coder" profile for implementation work +pub fn coder() -> AgentProfile { + AgentProfile { + name: "coder".to_string(), + extends: None, + role: "Implementation specialist".to_string(), + system_prompt: "You are an implementation specialist. Your role is to implement features and fix bugs by writing and modifying code. Follow the project's conventions and code style. Test your changes before marking tasks complete. Prioritize clarity and maintainability over clever solutions.".to_string(), + allowed_tools: vec![ + "file".to_string(), + "shell".to_string(), + "graph".to_string(), + "signal_completion".to_string(), + ], + security: SecurityScope { + allowed_paths: vec!["*".to_string()], + denied_paths: vec![], + allowed_commands: vec!["*".to_string()], + read_only: false, + can_create_files: true, + network_access: false, + }, + llm: ProfileLlmConfig::default(), + turn_limit: Some(100), + token_budget: Some(300_000), + } +} + +/// Built-in "reviewer" profile for code review +pub fn reviewer() -> AgentProfile { + AgentProfile { + name: "reviewer".to_string(), + extends: None, + role: "Code review specialist".to_string(), + system_prompt: "You are a code review specialist. Your role is to review code changes and provide constructive feedback. Check for: correctness, performance, security issues, adherence to project conventions, test coverage, and documentation. Point out both issues and good practices.".to_string(), + allowed_tools: vec![ + "file".to_string(), + "shell".to_string(), + "graph".to_string(), + "signal_completion".to_string(), + ], + security: SecurityScope { + allowed_paths: vec!["*".to_string()], + denied_paths: vec![], + allowed_commands: vec!["*".to_string()], + read_only: true, + can_create_files: false, + network_access: false, + }, + llm: ProfileLlmConfig::default(), + turn_limit: Some(100), + token_budget: Some(200_000), + } +} + +/// Built-in "tester" profile for test writing and quality assurance +pub fn tester() -> AgentProfile { + AgentProfile { + name: "tester".to_string(), + extends: None, + role: "Test implementation specialist".to_string(), + system_prompt: "You are a test implementation specialist. Your role is to write comprehensive tests including unit tests, integration tests, and edge cases. Ensure tests are clear, maintainable, and provide good coverage. Focus on testing behavior, not implementation details.".to_string(), + allowed_tools: vec![ + "file".to_string(), + "shell".to_string(), + "graph".to_string(), + "signal_completion".to_string(), + ], + security: SecurityScope { + allowed_paths: vec!["*".to_string()], + denied_paths: vec![], + allowed_commands: vec!["*".to_string()], + read_only: false, + can_create_files: true, + network_access: false, + }, + llm: ProfileLlmConfig::default(), + turn_limit: Some(100), + token_budget: Some(250_000), + } +} + +/// Built-in "researcher" profile for information gathering and investigation +pub fn researcher() -> AgentProfile { + AgentProfile { + name: "researcher".to_string(), + extends: None, + role: "Information gathering specialist".to_string(), + system_prompt: "You are an information gathering specialist. Your role is to investigate issues, gather requirements, explore solutions, and compile findings. Use available tools to explore the codebase, run searches, and gather context. Document your findings clearly.".to_string(), + allowed_tools: vec![ + "file".to_string(), + "shell".to_string(), + "graph".to_string(), + "signal_completion".to_string(), + ], + security: SecurityScope { + allowed_paths: vec!["*".to_string()], + denied_paths: vec![], + allowed_commands: vec!["*".to_string()], + read_only: true, + can_create_files: false, + network_access: false, + }, + llm: ProfileLlmConfig::default(), + turn_limit: Some(100), + token_budget: Some(200_000), + } +} diff --git a/src/agent/mod.rs b/src/agent/mod.rs index b67edc6..7bad4e9 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -1,4 +1,6 @@ +pub mod builtin_profiles; pub mod profile; +pub mod runtime; use crate::graph::GraphNode; use crate::graph::store::GraphStore; diff --git a/src/agent/profile.rs b/src/agent/profile.rs index 8fb2a58..1dcd94b 100644 --- a/src/agent/profile.rs +++ b/src/agent/profile.rs @@ -1,5 +1,8 @@ use crate::security::SecurityScope; +use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::path::Path; /// LLM configuration for an agent profile #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -115,3 +118,63 @@ impl AgentProfile { } } } + +/// Resolve a profile by name, checking in order: project-level, user-level, built-in. +/// +/// Supports inheritance via `extends` field. Returns error on cycles or unknown profiles. +pub fn resolve_profile(name: &str, project_path: Option<&Path>) -> Result { + let mut visited = HashSet::new(); + resolve_profile_impl(name, project_path, &mut visited) +} + +fn resolve_profile_impl( + name: &str, + project_path: Option<&Path>, + visited: &mut HashSet, +) -> Result { + // Check for cycles in inheritance + if visited.contains(name) { + bail!("inheritance cycle detected: profile '{}' extends itself", name); + } + visited.insert(name.to_string()); + + // 1. Project-level: .rustagent/profiles/{name}.toml + if let Some(path) = project_path { + let profile_path = path.join(".rustagent/profiles").join(format!("{}.toml", name)); + if profile_path.exists() { + let content = std::fs::read_to_string(&profile_path)?; + let mut profile: AgentProfile = toml::from_str(&content)?; + if let Some(parent_name) = &profile.extends.clone() { + let parent = resolve_profile_impl(parent_name, project_path, visited)?; + profile.apply_inheritance(&parent); + } + return Ok(profile); + } + } + + // 2. User-level: ~/.config/rustagent/profiles/{name}.toml + if let Some(config_dir) = dirs::config_dir() { + let profile_path = config_dir + .join("rustagent/profiles") + .join(format!("{}.toml", name)); + if profile_path.exists() { + let content = std::fs::read_to_string(&profile_path)?; + let mut profile: AgentProfile = toml::from_str(&content)?; + if let Some(parent_name) = &profile.extends.clone() { + let parent = resolve_profile_impl(parent_name, project_path, visited)?; + profile.apply_inheritance(&parent); + } + return Ok(profile); + } + } + + // 3. Built-in profiles + match name { + "planner" => Ok(crate::agent::builtin_profiles::planner()), + "coder" => Ok(crate::agent::builtin_profiles::coder()), + "reviewer" => Ok(crate::agent::builtin_profiles::reviewer()), + "tester" => Ok(crate::agent::builtin_profiles::tester()), + "researcher" => Ok(crate::agent::builtin_profiles::researcher()), + _ => bail!("Unknown profile: {}", name), + } +} diff --git a/src/agent/runtime.rs b/src/agent/runtime.rs new file mode 100644 index 0000000..d74f715 --- /dev/null +++ b/src/agent/runtime.rs @@ -0,0 +1,2 @@ +// AgentRuntime implementation - to be implemented in Task 6 +// This is a stub to allow compilation diff --git a/tests/profile_test.rs b/tests/profile_test.rs index 185e6d4..acc6917 100644 --- a/tests/profile_test.rs +++ b/tests/profile_test.rs @@ -1,5 +1,7 @@ -use rustagent::agent::profile::AgentProfile; +use rustagent::agent::profile::{AgentProfile, resolve_profile}; use rustagent::security::SecurityScope; +use std::fs; +use tempfile::TempDir; #[test] fn test_security_scope_default_is_permissive() { @@ -325,3 +327,239 @@ fn test_agent_profile_inheritance_llm_config() { assert_eq!(child.llm.temperature, Some(0.5)); // from parent assert_eq!(child.llm.max_tokens, Some(2000)); // from parent } + +// Built-in profiles tests + +#[test] +fn test_resolve_builtin_coder_profile() { + // P1d.AC3.2: resolve_profile("coder", None) returns built-in coder profile + let profile = resolve_profile("coder", None).expect("Failed to resolve coder profile"); + + assert_eq!(profile.name, "coder"); + assert_eq!(profile.role, "Implementation specialist"); + assert!(profile.system_prompt.len() > 0); + assert!(profile.allowed_tools.contains(&"file".to_string())); + assert!(profile.allowed_tools.contains(&"shell".to_string())); +} + +#[test] +fn test_resolve_builtin_planner_profile() { + // P1d.AC3.2: resolve_profile("planner", None) returns built-in planner profile + let profile = resolve_profile("planner", None).expect("Failed to resolve planner profile"); + + assert_eq!(profile.name, "planner"); + assert_eq!(profile.role, "Task breakdown specialist"); + assert!(profile.system_prompt.len() > 0); +} + +#[test] +fn test_resolve_builtin_reviewer_profile() { + // P1d.AC3.2: resolve_profile("reviewer", None) returns built-in reviewer profile + let profile = resolve_profile("reviewer", None).expect("Failed to resolve reviewer profile"); + + assert_eq!(profile.name, "reviewer"); + assert_eq!(profile.role, "Code review specialist"); + assert!(profile.system_prompt.len() > 0); +} + +#[test] +fn test_resolve_builtin_tester_profile() { + // P1d.AC3.2: resolve_profile("tester", None) returns built-in tester profile + let profile = resolve_profile("tester", None).expect("Failed to resolve tester profile"); + + assert_eq!(profile.name, "tester"); + assert_eq!(profile.role, "Test implementation specialist"); + assert!(profile.system_prompt.len() > 0); +} + +#[test] +fn test_resolve_builtin_researcher_profile() { + // P1d.AC3.2: resolve_profile("researcher", None) returns built-in researcher profile + let profile = resolve_profile("researcher", None).expect("Failed to resolve researcher profile"); + + assert_eq!(profile.name, "researcher"); + assert_eq!(profile.role, "Information gathering specialist"); + assert!(profile.system_prompt.len() > 0); +} + +#[test] +fn test_resolve_unknown_profile_fails() { + // Unknown profile should fail + let result = resolve_profile("nonexistent_profile", None); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Unknown profile")); +} + +#[test] +fn test_resolve_project_level_profile() { + // P1d.AC3.3: Create a tempdir with .rustagent/profiles/custom.toml + let tempdir = TempDir::new().expect("Failed to create tempdir"); + let project_path = tempdir.path(); + + // Create .rustagent/profiles directory + let profiles_dir = project_path.join(".rustagent").join("profiles"); + fs::create_dir_all(&profiles_dir).expect("Failed to create profiles directory"); + + // Create custom.toml + let custom_toml = r#" +name = "custom" +role = "Custom role" +system_prompt = "Custom system prompt" +allowed_tools = ["file", "shell"] +turn_limit = 50 +token_budget = 100000 + +[security] +allowed_paths = ["/project"] +denied_paths = [] +allowed_commands = ["ls", "cat"] +read_only = false +can_create_files = true +network_access = false + +[llm] +model = "claude-3-sonnet-20250219" +temperature = 0.7 +max_tokens = 4096 +"#; + + let profile_path = profiles_dir.join("custom.toml"); + fs::write(&profile_path, custom_toml).expect("Failed to write custom.toml"); + + let profile = + resolve_profile("custom", Some(project_path)).expect("Failed to resolve custom profile"); + + assert_eq!(profile.name, "custom"); + assert_eq!(profile.role, "Custom role"); +} + +#[test] +fn test_resolve_project_level_overrides_builtin() { + // P1d.AC3.4: Project-level "coder" profile should override built-in + let tempdir = TempDir::new().expect("Failed to create tempdir"); + let project_path = tempdir.path(); + + // Create .rustagent/profiles directory + let profiles_dir = project_path.join(".rustagent").join("profiles"); + fs::create_dir_all(&profiles_dir).expect("Failed to create profiles directory"); + + // Create project-level coder.toml + let project_coder = r#" +name = "coder" +role = "Project-specific coder" +system_prompt = "Project-specific system prompt" +allowed_tools = ["file", "shell"] + +[security] +allowed_paths = ["/project"] +denied_paths = [] +allowed_commands = ["*"] +read_only = false +can_create_files = true +network_access = false +"#; + + let profile_path = profiles_dir.join("coder.toml"); + fs::write(&profile_path, project_coder).expect("Failed to write coder.toml"); + + let profile = resolve_profile("coder", Some(project_path)) + .expect("Failed to resolve coder profile"); + + assert_eq!(profile.role, "Project-specific coder"); +} + +#[test] +fn test_resolve_profile_with_inheritance() { + // P1d.AC3.5: Custom profile extends built-in, inheritance applied + let tempdir = TempDir::new().expect("Failed to create tempdir"); + let project_path = tempdir.path(); + + // Create .rustagent/profiles directory + let profiles_dir = project_path.join(".rustagent").join("profiles"); + fs::create_dir_all(&profiles_dir).expect("Failed to create profiles directory"); + + // Create custom.toml that extends built-in "coder" + let custom_toml = r#" +name = "custom" +extends = "coder" +role = "" +system_prompt = "Custom project instructions" +allowed_tools = [] + +[security] +allowed_paths = ["*"] +denied_paths = [] +allowed_commands = ["*"] +read_only = false +can_create_files = true +network_access = false + +[llm] +"#; + + let profile_path = profiles_dir.join("custom.toml"); + fs::write(&profile_path, custom_toml).expect("Failed to write custom.toml"); + + let profile = resolve_profile("custom", Some(project_path)) + .expect("Failed to resolve custom profile"); + + // Should inherit role from coder (since custom is empty) + assert_eq!(profile.role, "Implementation specialist"); + // Should have coder's tools (since custom is empty) + assert!(profile.allowed_tools.contains(&"file".to_string())); + assert!(profile.allowed_tools.contains(&"shell".to_string())); + // Should have combined system_prompt + assert!(profile.system_prompt.contains("Custom project instructions")); +} + +#[test] +fn test_resolve_profile_cycle_detection() { + // P1d.AC3.5: Cycle detection in inheritance chain + let tempdir = TempDir::new().expect("Failed to create tempdir"); + let project_path = tempdir.path(); + + // Create .rustagent/profiles directory + let profiles_dir = project_path.join(".rustagent").join("profiles"); + fs::create_dir_all(&profiles_dir).expect("Failed to create profiles directory"); + + // Create a.toml that extends b + let a_toml = r#" +name = "a" +extends = "b" +role = "A" +system_prompt = "" +allowed_tools = [] + +[security] +allowed_paths = ["*"] +denied_paths = [] +allowed_commands = ["*"] +read_only = false +can_create_files = true +network_access = false +"#; + + // Create b.toml that extends a (cycle!) + let b_toml = r#" +name = "b" +extends = "a" +role = "B" +system_prompt = "" +allowed_tools = [] + +[security] +allowed_paths = ["*"] +denied_paths = [] +allowed_commands = ["*"] +read_only = false +can_create_files = true +network_access = false +"#; + + fs::write(profiles_dir.join("a.toml"), a_toml).expect("Failed to write a.toml"); + fs::write(profiles_dir.join("b.toml"), b_toml).expect("Failed to write b.toml"); + + let result = resolve_profile("a", Some(project_path)); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("cycle")); +}