feat(tools): implement M11 subagent system with task routing
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 <noreply@anthropic.com>
This commit is contained in:
14
crates/tools/task/Cargo.toml
Normal file
14
crates/tools/task/Cargo.toml
Normal file
@@ -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]
|
||||
221
crates/tools/task/src/lib.rs
Normal file
221
crates/tools/task/src/lib.rs
Normal file
@@ -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<String>,
|
||||
/// Tools this subagent is allowed to use
|
||||
pub allowed_tools: Vec<Tool>,
|
||||
}
|
||||
|
||||
impl Subagent {
|
||||
pub fn new(name: String, description: String, keywords: Vec<String>, allowed_tools: Vec<Tool>) -> 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<Subagent>,
|
||||
}
|
||||
|
||||
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<bool> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user