From f87e5d2796f1bd044ebffa1855e42b5e4ee198d9 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sat, 1 Nov 2025 20:37:37 +0100 Subject: [PATCH] feat(tools): implement M11 subagent system with task routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tools-task crate with subagent registry and tool whitelist system: Core Features: - Subagent struct with name, description, keywords, and allowed tools - SubagentRegistry for managing and selecting subagents - Tool whitelist validation per subagent - Keyword-based task matching and agent selection Built-in Subagents: - code-reviewer: Read-only code analysis (Read, Grep, Glob) - test-writer: Test file creation (Read, Write, Edit, Grep, Glob) - doc-writer: Documentation management (Read, Write, Edit, Grep, Glob) - refactorer: Code restructuring (Read, Write, Edit, Grep, Glob) Test Coverage: - Subagent tool whitelist enforcement - Keyword matching for task descriptions - Registry selection based on task description - Tool validation for specific agents - Error handling for nonexistent agents Implements M11 from AGENTS.md for specialized agents with limited tool access. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Cargo.toml | 1 + crates/tools/task/Cargo.toml | 14 +++ crates/tools/task/src/lib.rs | 221 +++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 crates/tools/task/Cargo.toml create mode 100644 crates/tools/task/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 5315bcf..290f709 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/tools/fs", "crates/tools/notebook", "crates/tools/slash", + "crates/tools/task", "crates/tools/web", "crates/integration/mcp-client", ] diff --git a/crates/tools/task/Cargo.toml b/crates/tools/task/Cargo.toml new file mode 100644 index 0000000..65a3de5 --- /dev/null +++ b/crates/tools/task/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tools-task" +version = "0.1.0" +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +color-eyre = "0.6" +permissions = { path = "../../platform/permissions" } + +[dev-dependencies] diff --git a/crates/tools/task/src/lib.rs b/crates/tools/task/src/lib.rs new file mode 100644 index 0000000..d3b4a8a --- /dev/null +++ b/crates/tools/task/src/lib.rs @@ -0,0 +1,221 @@ +use color_eyre::eyre::{Result, eyre}; +use permissions::Tool; +use serde::{Deserialize, Serialize}; + +/// A specialized subagent with limited tool access +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Subagent { + /// Unique identifier for the subagent + pub name: String, + /// Description of subagent's capabilities and purpose + pub description: String, + /// Keywords that trigger this subagent's selection + pub keywords: Vec, + /// Tools this subagent is allowed to use + pub allowed_tools: Vec, +} + +impl Subagent { + pub fn new(name: String, description: String, keywords: Vec, allowed_tools: Vec) -> Self { + Self { + name, + description, + keywords, + allowed_tools, + } + } + + /// Check if this subagent can use the specified tool + pub fn can_use_tool(&self, tool: Tool) -> bool { + self.allowed_tools.contains(&tool) + } + + /// Check if this subagent matches the task description + pub fn matches_task(&self, task_description: &str) -> bool { + let task_lower = task_description.to_lowercase(); + self.keywords.iter().any(|keyword| { + task_lower.contains(&keyword.to_lowercase()) + }) + } +} + +/// Registry for managing subagents +#[derive(Debug, Clone)] +pub struct SubagentRegistry { + subagents: Vec, +} + +impl SubagentRegistry { + pub fn new() -> Self { + Self { + subagents: Vec::new(), + } + } + + /// Register a new subagent + pub fn register(&mut self, subagent: Subagent) { + self.subagents.push(subagent); + } + + /// Select the most appropriate subagent for a task + pub fn select(&self, task_description: &str) -> Option<&Subagent> { + // Find the first subagent that matches the task description + self.subagents + .iter() + .find(|agent| agent.matches_task(task_description)) + } + + /// Get a subagent by name + pub fn get(&self, name: &str) -> Option<&Subagent> { + self.subagents.iter().find(|agent| agent.name == name) + } + + /// Check if a specific subagent can use a tool + pub fn can_use_tool(&self, agent_name: &str, tool: Tool) -> Result { + let agent = self.get(agent_name) + .ok_or_else(|| eyre!("Subagent '{}' not found", agent_name))?; + Ok(agent.can_use_tool(tool)) + } + + /// List all registered subagents + pub fn list(&self) -> &[Subagent] { + &self.subagents + } +} + +impl Default for SubagentRegistry { + fn default() -> Self { + let mut registry = Self::new(); + + // Register built-in subagents + + // Code reviewer - read-only tools + registry.register(Subagent::new( + "code-reviewer".to_string(), + "Reviews code for quality, bugs, and best practices".to_string(), + vec!["review".to_string(), "analyze code".to_string(), "check code".to_string()], + vec![Tool::Read, Tool::Grep, Tool::Glob], + )); + + // Test writer - can read and write test files + registry.register(Subagent::new( + "test-writer".to_string(), + "Writes and updates test files".to_string(), + vec!["test".to_string(), "write tests".to_string(), "add tests".to_string()], + vec![Tool::Read, Tool::Write, Tool::Edit, Tool::Grep, Tool::Glob], + )); + + // Documentation agent - can read code and write docs + registry.register(Subagent::new( + "doc-writer".to_string(), + "Writes and maintains documentation".to_string(), + vec!["document".to_string(), "docs".to_string(), "readme".to_string()], + vec![Tool::Read, Tool::Write, Tool::Edit, Tool::Grep, Tool::Glob], + )); + + // Refactoring agent - full file access but no bash + registry.register(Subagent::new( + "refactorer".to_string(), + "Refactors code while preserving functionality".to_string(), + vec!["refactor".to_string(), "restructure".to_string(), "reorganize".to_string()], + vec![Tool::Read, Tool::Write, Tool::Edit, Tool::Grep, Tool::Glob], + )); + + registry + } +} + +/// Task execution request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskRequest { + /// Description of the task to execute + pub description: String, + /// Optional: specific subagent to use + pub agent_name: Option, +} + +/// Task execution result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskResult { + /// The subagent that handled the task + pub agent_name: String, + /// Success or failure + pub success: bool, + /// Result message + pub message: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn subagent_tool_whitelist() { + let agent = Subagent::new( + "reader".to_string(), + "Read-only agent".to_string(), + vec!["read".to_string()], + vec![Tool::Read, Tool::Grep], + ); + + assert!(agent.can_use_tool(Tool::Read)); + assert!(agent.can_use_tool(Tool::Grep)); + assert!(!agent.can_use_tool(Tool::Write)); + assert!(!agent.can_use_tool(Tool::Bash)); + } + + #[test] + fn subagent_keyword_matching() { + let agent = Subagent::new( + "tester".to_string(), + "Test agent".to_string(), + vec!["test".to_string(), "unit test".to_string()], + vec![Tool::Read, Tool::Write], + ); + + assert!(agent.matches_task("Write unit tests for the auth module")); + assert!(agent.matches_task("Add test coverage")); + assert!(!agent.matches_task("Refactor the database layer")); + } + + #[test] + fn registry_selection() { + let registry = SubagentRegistry::default(); + + let reviewer = registry.select("Review the authentication code"); + assert!(reviewer.is_some()); + assert_eq!(reviewer.unwrap().name, "code-reviewer"); + + let tester = registry.select("Write tests for the API endpoints"); + assert!(tester.is_some()); + assert_eq!(tester.unwrap().name, "test-writer"); + + let doc_writer = registry.select("Update the README documentation"); + assert!(doc_writer.is_some()); + assert_eq!(doc_writer.unwrap().name, "doc-writer"); + } + + #[test] + fn registry_tool_validation() { + let registry = SubagentRegistry::default(); + + // Code reviewer can only use read-only tools + assert!(registry.can_use_tool("code-reviewer", Tool::Read).unwrap()); + assert!(registry.can_use_tool("code-reviewer", Tool::Grep).unwrap()); + assert!(!registry.can_use_tool("code-reviewer", Tool::Write).unwrap()); + assert!(!registry.can_use_tool("code-reviewer", Tool::Bash).unwrap()); + + // Test writer can write but not run bash + assert!(registry.can_use_tool("test-writer", Tool::Read).unwrap()); + assert!(registry.can_use_tool("test-writer", Tool::Write).unwrap()); + assert!(!registry.can_use_tool("test-writer", Tool::Bash).unwrap()); + } + + #[test] + fn nonexistent_agent_error() { + let registry = SubagentRegistry::default(); + let result = registry.can_use_tool("nonexistent", Tool::Read); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("not found")); + } +}