Multi-LLM Provider Support: - Add llm-core crate with LlmProvider trait abstraction - Implement Anthropic Claude API client with streaming - Implement OpenAI API client with streaming - Add token counting with SimpleTokenCounter and ClaudeTokenCounter - Add retry logic with exponential backoff and jitter Borderless TUI Redesign: - Rewrite theme system with terminal capability detection (Full/Unicode256/Basic) - Add provider tabs component with keybind switching [1]/[2]/[3] - Implement vim-modal input (Normal/Insert/Visual/Command modes) - Redesign chat panel with timestamps and streaming indicators - Add multi-provider status bar with cost tracking - Add Nerd Font icons with graceful ASCII fallbacks - Add syntax highlighting (syntect) and markdown rendering (pulldown-cmark) Advanced Agent Features: - Add system prompt builder with configurable components - Enhance subagent orchestration with parallel execution - Add git integration module for safe command detection - Add streaming tool results via channels - Expand tool set: AskUserQuestion, TodoWrite, LS, MultiEdit, BashOutput, KillShell - Add WebSearch with provider abstraction Plugin System Enhancement: - Add full agent definition parsing from YAML frontmatter - Add skill system with progressive disclosure - Wire plugin hooks into HookManager 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
336 lines
12 KiB
Rust
336 lines
12 KiB
Rust
// Note: Result and eyre will be used by spawn_subagent when implemented
|
|
#[allow(unused_imports)]
|
|
use color_eyre::eyre::{Result, eyre};
|
|
use parking_lot::RwLock;
|
|
use permissions::Tool;
|
|
use plugins::AgentDefinition;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
/// Configuration for spawning a subagent
|
|
#[derive(Debug, Clone)]
|
|
pub struct SubagentConfig {
|
|
/// Agent type/name (e.g., "code-reviewer", "explore")
|
|
pub agent_type: String,
|
|
|
|
/// Task prompt for the agent
|
|
pub prompt: String,
|
|
|
|
/// Optional model override
|
|
pub model: Option<String>,
|
|
|
|
/// Tool whitelist (if None, uses agent's default)
|
|
pub tools: Option<Vec<String>>,
|
|
|
|
/// Parsed agent definition (if from plugin)
|
|
pub definition: Option<AgentDefinition>,
|
|
}
|
|
|
|
impl SubagentConfig {
|
|
/// Create a new subagent config with just type and prompt
|
|
pub fn new(agent_type: String, prompt: String) -> Self {
|
|
Self {
|
|
agent_type,
|
|
prompt,
|
|
model: None,
|
|
tools: None,
|
|
definition: None,
|
|
}
|
|
}
|
|
|
|
/// Builder method to set model override
|
|
pub fn with_model(mut self, model: String) -> Self {
|
|
self.model = Some(model);
|
|
self
|
|
}
|
|
|
|
/// Builder method to set tool whitelist
|
|
pub fn with_tools(mut self, tools: Vec<String>) -> Self {
|
|
self.tools = Some(tools);
|
|
self
|
|
}
|
|
|
|
/// Builder method to set agent definition
|
|
pub fn with_definition(mut self, definition: AgentDefinition) -> Self {
|
|
self.definition = Some(definition);
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Registry of available subagents
|
|
#[derive(Clone, Default)]
|
|
pub struct SubagentRegistry {
|
|
agents: Arc<RwLock<HashMap<String, AgentDefinition>>>,
|
|
}
|
|
|
|
impl SubagentRegistry {
|
|
/// Create a new empty registry
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Register agents from plugin manager
|
|
pub fn register_from_plugins(&self, agents: Vec<AgentDefinition>) {
|
|
let mut map = self.agents.write();
|
|
for agent in agents {
|
|
map.insert(agent.name.clone(), agent);
|
|
}
|
|
}
|
|
|
|
/// Register built-in agents
|
|
pub fn register_builtin(&self) {
|
|
let mut map = self.agents.write();
|
|
|
|
// Explore agent - for codebase exploration
|
|
map.insert("explore".to_string(), AgentDefinition {
|
|
name: "explore".to_string(),
|
|
description: "Explores codebases to find files and understand structure".to_string(),
|
|
tools: vec!["read".to_string(), "glob".to_string(), "grep".to_string(), "ls".to_string()],
|
|
model: None,
|
|
color: Some("blue".to_string()),
|
|
system_prompt: "You are an exploration agent. Your purpose is to find relevant files and understand code structure. Use glob to find files by pattern, grep to search for content, ls to list directories, and read to examine files. Be thorough and systematic in your exploration.".to_string(),
|
|
source_path: PathBuf::new(),
|
|
});
|
|
|
|
// Plan agent - for designing implementations
|
|
map.insert("plan".to_string(), AgentDefinition {
|
|
name: "plan".to_string(),
|
|
description: "Designs implementation plans and architectures".to_string(),
|
|
tools: vec!["read".to_string(), "glob".to_string(), "grep".to_string()],
|
|
model: None,
|
|
color: Some("green".to_string()),
|
|
system_prompt: "You are a planning agent. Your purpose is to design clear implementation strategies and architectures. Read existing code, understand patterns, and create detailed plans. Focus on the 'why' and 'how' rather than the 'what'.".to_string(),
|
|
source_path: PathBuf::new(),
|
|
});
|
|
|
|
// Code reviewer - read-only analysis
|
|
map.insert("code-reviewer".to_string(), AgentDefinition {
|
|
name: "code-reviewer".to_string(),
|
|
description: "Reviews code for quality, bugs, and best practices".to_string(),
|
|
tools: vec!["read".to_string(), "grep".to_string(), "glob".to_string()],
|
|
model: None,
|
|
color: Some("yellow".to_string()),
|
|
system_prompt: "You are a code review agent. Analyze code for quality, potential bugs, performance issues, and adherence to best practices. Provide constructive feedback with specific examples.".to_string(),
|
|
source_path: PathBuf::new(),
|
|
});
|
|
|
|
// Test writer - can read and write test files
|
|
map.insert("test-writer".to_string(), AgentDefinition {
|
|
name: "test-writer".to_string(),
|
|
description: "Writes and updates test files".to_string(),
|
|
tools: vec!["read".to_string(), "write".to_string(), "edit".to_string(), "grep".to_string(), "glob".to_string()],
|
|
model: None,
|
|
color: Some("cyan".to_string()),
|
|
system_prompt: "You are a test writing agent. Write comprehensive, well-structured tests that cover edge cases and ensure code correctness. Follow testing best practices and patterns used in the codebase.".to_string(),
|
|
source_path: PathBuf::new(),
|
|
});
|
|
|
|
// Documentation agent - can read code and write docs
|
|
map.insert("doc-writer".to_string(), AgentDefinition {
|
|
name: "doc-writer".to_string(),
|
|
description: "Writes and maintains documentation".to_string(),
|
|
tools: vec!["read".to_string(), "write".to_string(), "edit".to_string(), "grep".to_string(), "glob".to_string()],
|
|
model: None,
|
|
color: Some("magenta".to_string()),
|
|
system_prompt: "You are a documentation agent. Write clear, comprehensive documentation that helps users understand the code. Include examples, explain concepts, and maintain consistency with existing documentation style.".to_string(),
|
|
source_path: PathBuf::new(),
|
|
});
|
|
|
|
// Refactoring agent - full file access but no bash
|
|
map.insert("refactorer".to_string(), AgentDefinition {
|
|
name: "refactorer".to_string(),
|
|
description: "Refactors code while preserving functionality".to_string(),
|
|
tools: vec!["read".to_string(), "write".to_string(), "edit".to_string(), "grep".to_string(), "glob".to_string()],
|
|
model: None,
|
|
color: Some("red".to_string()),
|
|
system_prompt: "You are a refactoring agent. Improve code structure, readability, and maintainability while preserving functionality. Follow SOLID principles and language idioms. Make small, incremental changes.".to_string(),
|
|
source_path: PathBuf::new(),
|
|
});
|
|
}
|
|
|
|
/// Get an agent by name
|
|
pub fn get(&self, name: &str) -> Option<AgentDefinition> {
|
|
self.agents.read().get(name).cloned()
|
|
}
|
|
|
|
/// List all available agents with their descriptions
|
|
pub fn list(&self) -> Vec<(String, String)> {
|
|
self.agents.read()
|
|
.iter()
|
|
.map(|(name, def)| (name.clone(), def.description.clone()))
|
|
.collect()
|
|
}
|
|
|
|
/// Check if an agent exists
|
|
pub fn contains(&self, name: &str) -> bool {
|
|
self.agents.read().contains_key(name)
|
|
}
|
|
|
|
/// Get all agent names
|
|
pub fn agent_names(&self) -> Vec<String> {
|
|
self.agents.read().keys().cloned().collect()
|
|
}
|
|
}
|
|
|
|
/// A specialized subagent with limited tool access (legacy API for backward compatibility)
|
|
#[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())
|
|
})
|
|
}
|
|
}
|
|
|
|
/// 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 test_subagent_registry_builtin() {
|
|
let registry = SubagentRegistry::new();
|
|
registry.register_builtin();
|
|
|
|
// Check that built-in agents are registered
|
|
assert!(registry.contains("explore"));
|
|
assert!(registry.contains("plan"));
|
|
assert!(registry.contains("code-reviewer"));
|
|
assert!(registry.contains("test-writer"));
|
|
assert!(registry.contains("doc-writer"));
|
|
assert!(registry.contains("refactorer"));
|
|
|
|
// Get an agent and verify its properties
|
|
let explore = registry.get("explore").unwrap();
|
|
assert_eq!(explore.name, "explore");
|
|
assert!(explore.tools.contains(&"read".to_string()));
|
|
assert!(explore.tools.contains(&"glob".to_string()));
|
|
assert!(explore.tools.contains(&"grep".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_subagent_registry_list() {
|
|
let registry = SubagentRegistry::new();
|
|
registry.register_builtin();
|
|
|
|
let agents = registry.list();
|
|
assert!(agents.len() >= 6);
|
|
|
|
// Check that we have expected agents
|
|
let names: Vec<String> = agents.iter().map(|(name, _)| name.clone()).collect();
|
|
assert!(names.contains(&"explore".to_string()));
|
|
assert!(names.contains(&"plan".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_subagent_config_builder() {
|
|
let config = SubagentConfig::new("explore".to_string(), "Find all Rust files".to_string())
|
|
.with_model("claude-3-opus".to_string())
|
|
.with_tools(vec!["read".to_string(), "glob".to_string()]);
|
|
|
|
assert_eq!(config.agent_type, "explore");
|
|
assert_eq!(config.prompt, "Find all Rust files");
|
|
assert_eq!(config.model, Some("claude-3-opus".to_string()));
|
|
assert_eq!(config.tools, Some(vec!["read".to_string(), "glob".to_string()]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_register_from_plugins() {
|
|
let registry = SubagentRegistry::new();
|
|
|
|
let plugin_agent = AgentDefinition {
|
|
name: "custom-agent".to_string(),
|
|
description: "A custom agent from plugin".to_string(),
|
|
tools: vec!["read".to_string()],
|
|
model: Some("haiku".to_string()),
|
|
color: Some("purple".to_string()),
|
|
system_prompt: "Custom prompt".to_string(),
|
|
source_path: PathBuf::from("/path/to/plugin"),
|
|
};
|
|
|
|
registry.register_from_plugins(vec![plugin_agent]);
|
|
|
|
assert!(registry.contains("custom-agent"));
|
|
let agent = registry.get("custom-agent").unwrap();
|
|
assert_eq!(agent.model, Some("haiku".to_string()));
|
|
}
|
|
|
|
// Legacy API tests for backward compatibility
|
|
#[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"));
|
|
}
|
|
}
|