use crate::config::CodeConfig;
use crate::permissions::{PermissionChecker, PermissionDecision, PermissionPolicy};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::RwLock;
use crate::error::{read_or_recover, write_or_recover};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ConfirmationInheritance {
#[default]
AutoApprove,
DenyOnAsk,
InheritParent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
pub model: String,
pub provider: Option<String>,
}
impl ModelConfig {
pub fn new(model: impl Into<String>) -> Self {
Self {
model: model.into(),
provider: None,
}
}
pub fn with_provider(provider: impl Into<String>, model: impl Into<String>) -> Self {
Self {
model: model.into(),
provider: Some(provider.into()),
}
}
pub fn from_model_ref(model_ref: impl AsRef<str>) -> Self {
let model_ref = model_ref.as_ref();
if let Some((provider, model)) = model_ref.split_once('/') {
Self::with_provider(provider, model)
} else {
Self::new(model_ref)
}
}
pub fn model_ref(&self) -> String {
match &self.provider {
Some(provider) => format!("{}/{}", provider, self.model),
None => self.model.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerAgentKind {
#[serde(alias = "readonly", alias = "read-only", alias = "explore")]
ReadOnly,
#[serde(alias = "plan")]
Planner,
#[serde(alias = "implementation", alias = "general")]
Implementer,
#[serde(alias = "verification", alias = "verify")]
Verifier,
#[serde(alias = "review", alias = "code-review")]
Reviewer,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerAgentSpec {
pub name: String,
pub description: String,
pub kind: WorkerAgentKind,
#[serde(default)]
pub hidden: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<PermissionPolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<ModelConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_steps: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confirmation_inheritance: Option<ConfirmationInheritance>,
}
impl WorkerAgentKind {
pub fn as_str(self) -> &'static str {
match self {
Self::ReadOnly => "read_only",
Self::Planner => "planner",
Self::Implementer => "implementer",
Self::Verifier => "verifier",
Self::Reviewer => "reviewer",
Self::Custom => "custom",
}
}
fn default_permissions(self) -> PermissionPolicy {
match self {
Self::ReadOnly => explore_permissions(),
Self::Planner => plan_permissions(),
Self::Implementer => general_permissions(),
Self::Verifier => verification_permissions(),
Self::Reviewer => review_permissions(),
Self::Custom => PermissionPolicy::strict(),
}
}
fn default_prompt(self) -> Option<&'static str> {
match self {
Self::ReadOnly => Some(EXPLORE_PROMPT),
Self::Planner => Some(PLAN_PROMPT),
Self::Verifier => Some(VERIFICATION_PROMPT),
Self::Reviewer => Some(REVIEW_PROMPT),
Self::Implementer | Self::Custom => None,
}
}
fn default_max_steps(self) -> usize {
match self {
Self::ReadOnly => 20,
Self::Planner => 30,
Self::Implementer => 50,
Self::Verifier => 30,
Self::Reviewer => 25,
Self::Custom => 30,
}
}
}
impl std::fmt::Display for WorkerAgentKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for WorkerAgentKind {
type Err = anyhow::Error;
fn from_str(value: &str) -> anyhow::Result<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"read_only" | "readonly" | "read-only" | "explore" | "scanner" => Ok(Self::ReadOnly),
"planner" | "plan" => Ok(Self::Planner),
"implementer" | "implementation" | "general" | "executor" => Ok(Self::Implementer),
"verifier" | "verification" | "verify" | "tester" => Ok(Self::Verifier),
"reviewer" | "review" | "code-review" | "code_reviewer" => Ok(Self::Reviewer),
"custom" => Ok(Self::Custom),
other => Err(anyhow::anyhow!("unknown worker agent kind '{}'", other)),
}
}
}
pub type CattleAgentKind = WorkerAgentKind;
pub type CattleAgentSpec = WorkerAgentSpec;
impl WorkerAgentSpec {
pub fn new(
kind: WorkerAgentKind,
name: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
kind,
hidden: false,
permissions: None,
model: None,
prompt: None,
max_steps: None,
confirmation_inheritance: None,
}
}
pub fn read_only(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(WorkerAgentKind::ReadOnly, name, description)
}
pub fn planner(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(WorkerAgentKind::Planner, name, description)
}
pub fn implementer(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(WorkerAgentKind::Implementer, name, description)
}
pub fn verifier(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(WorkerAgentKind::Verifier, name, description)
}
pub fn reviewer(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(WorkerAgentKind::Reviewer, name, description)
}
pub fn custom(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(WorkerAgentKind::Custom, name, description)
}
pub fn hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
self.permissions = Some(permissions);
self
}
pub fn with_model(mut self, model: ModelConfig) -> Self {
self.model = Some(model);
self
}
pub fn with_model_ref(mut self, model_ref: impl AsRef<str>) -> Self {
self.model = Some(ModelConfig::from_model_ref(model_ref));
self
}
pub fn with_provider_model(
mut self,
provider: impl Into<String>,
model: impl Into<String>,
) -> Self {
self.model = Some(ModelConfig::with_provider(provider, model));
self
}
pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn with_max_steps(mut self, max_steps: usize) -> Self {
self.max_steps = Some(max_steps);
self
}
pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
self.confirmation_inheritance = Some(inheritance);
self
}
pub fn into_agent_definition(self) -> AgentDefinition {
let mut agent = AgentDefinition::new(&self.name, &self.description)
.with_permissions(
self.permissions
.unwrap_or_else(|| self.kind.default_permissions()),
)
.with_max_steps(
self.max_steps
.unwrap_or_else(|| self.kind.default_max_steps()),
);
if self.hidden {
agent = agent.hidden();
}
if let Some(model) = self.model {
agent = agent.with_model(model);
}
if let Some(prompt) = self
.prompt
.or_else(|| self.kind.default_prompt().map(str::to_string))
{
agent = agent.with_prompt(&prompt);
}
if let Some(ci) = self.confirmation_inheritance {
agent = agent.with_confirmation(ci);
}
agent
}
}
impl From<WorkerAgentSpec> for AgentDefinition {
fn from(spec: WorkerAgentSpec) -> Self {
spec.into_agent_definition()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub name: String,
pub description: String,
#[serde(default)]
pub native: bool,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub permissions: PermissionPolicy,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<ModelConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_steps: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confirmation_inheritance: Option<ConfirmationInheritance>,
}
impl AgentDefinition {
pub fn new(name: &str, description: &str) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
native: false,
hidden: false,
permissions: PermissionPolicy::default(),
model: None,
prompt: None,
max_steps: None,
confirmation_inheritance: None,
}
}
pub fn worker(spec: WorkerAgentSpec) -> Self {
spec.into_agent_definition()
}
pub fn native(mut self) -> Self {
self.native = true;
self
}
pub fn hidden(mut self) -> Self {
self.hidden = true;
self
}
pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
self.permissions = permissions;
self
}
pub fn with_model(mut self, model: ModelConfig) -> Self {
self.model = Some(model);
self
}
pub fn with_prompt(mut self, prompt: &str) -> Self {
self.prompt = Some(prompt.to_string());
self
}
pub fn with_max_steps(mut self, max_steps: usize) -> Self {
self.max_steps = Some(max_steps);
self
}
pub fn has_defined_permissions(&self) -> bool {
!self.permissions.allow.is_empty() || !self.permissions.deny.is_empty()
}
pub(crate) fn apply_to(&self, config: &mut crate::agent::AgentConfig) {
use std::sync::Arc;
if config.permission_checker.is_none() && self.has_defined_permissions() {
config.permission_checker =
Some(Arc::new(self.permissions.clone()) as Arc<dyn PermissionChecker>);
config.permission_policy = Some(self.permissions.clone());
}
if let Some(ref prompt) = self.prompt {
if config.prompt_slots.extra.is_none() {
config.prompt_slots.extra = Some(prompt.clone());
}
}
if let Some(max_steps) = self.max_steps {
if config.max_tool_rounds == crate::agent::MAX_TOOL_ROUNDS {
config.max_tool_rounds = max_steps;
}
}
if config.confirmation_manager.is_none() {
let inheritance = self.confirmation_inheritance.clone().unwrap_or_else(|| {
if self.has_defined_permissions() {
ConfirmationInheritance::AutoApprove
} else {
ConfirmationInheritance::DenyOnAsk
}
});
match inheritance {
ConfirmationInheritance::AutoApprove => {
config.confirmation_manager =
Some(Arc::new(crate::hitl::AutoApproveConfirmation));
}
ConfirmationInheritance::DenyOnAsk => { }
ConfirmationInheritance::InheritParent => { }
}
}
}
pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
self.confirmation_inheritance = Some(inheritance);
self
}
}
pub struct AgentRegistry {
agents: RwLock<HashMap<String, AgentDefinition>>,
}
fn canonical_agent_name(name: &str) -> &str {
match name.trim() {
"general-purpose" | "general_purpose" | "generalpurpose" => "general",
"deep_research" | "deepresearch" => "deep-research",
"verify" | "verifier" => "verification",
"code-review" | "code_reviewer" | "reviewer" => "review",
other => other,
}
}
impl Default for AgentRegistry {
fn default() -> Self {
Self::new()
}
}
impl AgentRegistry {
pub fn new() -> Self {
let registry = Self {
agents: RwLock::new(HashMap::new()),
};
for agent in builtin_agents() {
registry.register(agent);
}
registry
}
pub fn with_config(config: &CodeConfig) -> Self {
let registry = Self::new();
for dir in &config.agent_dirs {
let agents = load_agents_from_dir(dir);
for agent in agents {
tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
registry.register(agent);
}
}
registry
}
pub fn register(&self, agent: AgentDefinition) {
let mut agents = write_or_recover(&self.agents);
tracing::debug!("Registering agent: {}", agent.name);
agents.insert(agent.name.clone(), agent);
}
pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
let agent = spec.into_agent_definition();
self.register(agent.clone());
agent
}
pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
where
I: IntoIterator<Item = WorkerAgentSpec>,
{
specs
.into_iter()
.map(|spec| self.register_worker(spec))
.collect()
}
pub fn unregister(&self, name: &str) -> bool {
let mut agents = write_or_recover(&self.agents);
agents.remove(name).is_some()
}
pub fn get(&self, name: &str) -> Option<AgentDefinition> {
let agents = read_or_recover(&self.agents);
agents
.get(name)
.or_else(|| agents.get(canonical_agent_name(name)))
.cloned()
}
pub fn list(&self) -> Vec<AgentDefinition> {
let agents = read_or_recover(&self.agents);
agents.values().cloned().collect()
}
pub fn list_visible(&self) -> Vec<AgentDefinition> {
let agents = read_or_recover(&self.agents);
agents.values().filter(|a| !a.hidden).cloned().collect()
}
pub fn exists(&self, name: &str) -> bool {
let agents = read_or_recover(&self.agents);
agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
}
pub fn len(&self) -> usize {
let agents = read_or_recover(&self.agents);
agents.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[path = "subagent/loader.rs"]
mod loader;
pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
pub fn builtin_agents() -> Vec<AgentDefinition> {
vec![
AgentDefinition::new(
"explore",
"Fast read-only exploration agent. Use for searching files, reading code, \
understanding codebase structure, and gathering external web evidence.",
)
.native()
.with_permissions(explore_permissions())
.with_max_steps(20)
.with_prompt(EXPLORE_PROMPT),
AgentDefinition::new(
"general",
"General-purpose agent for multi-step task execution. Can read, write, \
and execute commands.",
)
.native()
.with_permissions(general_permissions())
.with_max_steps(50),
AgentDefinition::new(
"deep-research",
"DeepResearch evidence agent. Inherits the parent session's tools, \
skills, permissions, and HITL policy for bounded evidence collection.",
)
.native()
.hidden()
.with_confirmation(ConfirmationInheritance::InheritParent)
.with_max_steps(50),
AgentDefinition::new(
"plan",
"Planning agent for designing implementation approaches. Read-only access \
to explore codebase and create plans.",
)
.native()
.with_permissions(plan_permissions())
.with_max_steps(30)
.with_prompt(PLAN_PROMPT),
AgentDefinition::new(
"verification",
"Verification agent for adversarial validation. Prefer real checks, \
reproductions, and regression testing over code reading alone.",
)
.native()
.with_permissions(verification_permissions())
.with_max_steps(30)
.with_prompt(VERIFICATION_PROMPT),
AgentDefinition::new(
"review",
"Code review agent focused on correctness, regressions, security, \
maintainability, and clear findings.",
)
.native()
.with_permissions(review_permissions())
.with_max_steps(25)
.with_prompt(REVIEW_PROMPT),
]
}
fn explore_permissions() -> PermissionPolicy {
let mut policy = PermissionPolicy::new()
.allow_all(&["read", "grep", "glob", "ls", "web_fetch", "web_search"])
.deny_all(&["write", "edit", "task", "parallel_task"])
.allow("Bash(ls:*)")
.allow("Bash(cat:*)")
.allow("Bash(head:*)")
.allow("Bash(tail:*)")
.allow("Bash(find:*)")
.allow("Bash(wc:*)")
.deny("Bash(rm:*)")
.deny("Bash(mv:*)")
.deny("Bash(cp:*)");
policy.default_decision = PermissionDecision::Deny;
policy
}
fn general_permissions() -> PermissionPolicy {
PermissionPolicy::new()
.allow_all(&[
"read",
"write",
"edit",
"grep",
"glob",
"ls",
"bash",
"web_fetch",
"web_search",
"git",
"patch",
"batch",
"generate_object",
])
.deny("task")
.deny("parallel_task")
}
fn plan_permissions() -> PermissionPolicy {
let mut policy = PermissionPolicy::new()
.allow_all(&["read", "grep", "glob", "ls"])
.deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
policy.default_decision = PermissionDecision::Deny;
policy
}
fn verification_permissions() -> PermissionPolicy {
let mut policy = PermissionPolicy::new()
.allow_all(&[
"read",
"grep",
"glob",
"ls",
"bash",
"web_fetch",
"web_search",
])
.deny_all(&["write", "edit", "task", "parallel_task"]);
policy.default_decision = PermissionDecision::Deny;
policy
}
fn review_permissions() -> PermissionPolicy {
let mut policy = PermissionPolicy::new()
.allow_all(&[
"read",
"grep",
"glob",
"ls",
"bash",
"web_fetch",
"web_search",
])
.deny_all(&["write", "edit", "task", "parallel_task"]);
policy.default_decision = PermissionDecision::Deny;
policy
}
const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
#[cfg(test)]
#[path = "subagent/tests.rs"]
mod tests;