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:
2025-11-01 20:37:37 +01:00
parent 3c436fda54
commit f87e5d2796
3 changed files with 236 additions and 0 deletions

View 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]

View 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"));
}
}