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)]
pub tool_free: bool,
#[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,
tool_free: false,
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 tool_free(mut self) -> Self {
self.tool_free = true;
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 self.tool_free {
config.tools.clear();
let policy = PermissionPolicy::strict();
config.permission_checker = Some(Arc::new(policy.clone()));
config.permission_policy = Some(policy);
}
if !self.tool_free && 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(
"loop-planner",
"Tool-free semantic planner for engineered loops.",
)
.native()
.hidden()
.tool_free()
.with_max_steps(4)
.with_prompt(LOOP_PLANNER_PROMPT),
AgentDefinition::new(
"loop-checker",
"Tool-free independent checker for engineered loops.",
)
.native()
.hidden()
.tool_free()
.with_max_steps(4)
.with_prompt(LOOP_CHECKER_PROMPT),
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;
const LOOP_PLANNER_PROMPT: &str = "You are the planner in an engineered loop. Make the requested structured planning decision directly from the supplied goal and constraints. You have no tools and must not request tool calls. Do not collect evidence or execute the plan.";
const LOOP_CHECKER_PROMPT: &str = "You are the independent checker in an engineered loop. Evaluate only the supplied plan and maker evidence, then return the requested structured decision. You have no tools and must not request tool calls or gather new evidence.";
#[cfg(test)]
#[path = "subagent/tests.rs"]
mod tests;