1use crate::config::CodeConfig;
59use crate::permissions::{PermissionChecker, PermissionDecision, PermissionPolicy};
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62use std::path::Path;
63use std::sync::RwLock;
64
65use crate::error::{read_or_recover, write_or_recover};
66
67#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
69#[serde(rename_all = "snake_case")]
70pub enum ConfirmationInheritance {
71 #[default]
74 AutoApprove,
75 DenyOnAsk,
77 InheritParent,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ModelConfig {
84 pub model: String,
86 pub provider: Option<String>,
88}
89
90impl ModelConfig {
91 pub fn new(model: impl Into<String>) -> Self {
93 Self {
94 model: model.into(),
95 provider: None,
96 }
97 }
98
99 pub fn with_provider(provider: impl Into<String>, model: impl Into<String>) -> Self {
101 Self {
102 model: model.into(),
103 provider: Some(provider.into()),
104 }
105 }
106
107 pub fn from_model_ref(model_ref: impl AsRef<str>) -> Self {
112 let model_ref = model_ref.as_ref();
113 if let Some((provider, model)) = model_ref.split_once('/') {
114 Self::with_provider(provider, model)
115 } else {
116 Self::new(model_ref)
117 }
118 }
119
120 pub fn model_ref(&self) -> String {
122 match &self.provider {
123 Some(provider) => format!("{}/{}", provider, self.model),
124 None => self.model.clone(),
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum WorkerAgentKind {
137 #[serde(alias = "readonly", alias = "read-only", alias = "explore")]
139 ReadOnly,
140 #[serde(alias = "plan")]
142 Planner,
143 #[serde(alias = "implementation", alias = "general")]
145 Implementer,
146 #[serde(alias = "verification", alias = "verify")]
148 Verifier,
149 #[serde(alias = "review", alias = "code-review")]
151 Reviewer,
152 Custom,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct WorkerAgentSpec {
163 pub name: String,
165 pub description: String,
167 pub kind: WorkerAgentKind,
169 #[serde(default)]
171 pub hidden: bool,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub permissions: Option<PermissionPolicy>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub model: Option<ModelConfig>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub prompt: Option<String>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub max_steps: Option<usize>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub confirmation_inheritance: Option<ConfirmationInheritance>,
187}
188
189impl WorkerAgentKind {
190 pub fn as_str(self) -> &'static str {
192 match self {
193 Self::ReadOnly => "read_only",
194 Self::Planner => "planner",
195 Self::Implementer => "implementer",
196 Self::Verifier => "verifier",
197 Self::Reviewer => "reviewer",
198 Self::Custom => "custom",
199 }
200 }
201
202 fn default_permissions(self) -> PermissionPolicy {
203 match self {
204 Self::ReadOnly => explore_permissions(),
205 Self::Planner => plan_permissions(),
206 Self::Implementer => general_permissions(),
207 Self::Verifier => verification_permissions(),
208 Self::Reviewer => review_permissions(),
209 Self::Custom => PermissionPolicy::strict(),
210 }
211 }
212
213 fn default_prompt(self) -> Option<&'static str> {
214 match self {
215 Self::ReadOnly => Some(EXPLORE_PROMPT),
216 Self::Planner => Some(PLAN_PROMPT),
217 Self::Verifier => Some(VERIFICATION_PROMPT),
218 Self::Reviewer => Some(REVIEW_PROMPT),
219 Self::Implementer | Self::Custom => None,
220 }
221 }
222
223 fn default_max_steps(self) -> usize {
224 match self {
225 Self::ReadOnly => 20,
226 Self::Planner => 30,
227 Self::Implementer => 50,
228 Self::Verifier => 30,
229 Self::Reviewer => 25,
230 Self::Custom => 30,
231 }
232 }
233}
234
235impl std::fmt::Display for WorkerAgentKind {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 f.write_str(self.as_str())
238 }
239}
240
241impl std::str::FromStr for WorkerAgentKind {
242 type Err = anyhow::Error;
243
244 fn from_str(value: &str) -> anyhow::Result<Self> {
245 match value.trim().to_ascii_lowercase().as_str() {
246 "read_only" | "readonly" | "read-only" | "explore" | "scanner" => Ok(Self::ReadOnly),
247 "planner" | "plan" => Ok(Self::Planner),
248 "implementer" | "implementation" | "general" | "executor" => Ok(Self::Implementer),
249 "verifier" | "verification" | "verify" | "tester" => Ok(Self::Verifier),
250 "reviewer" | "review" | "code-review" | "code_reviewer" => Ok(Self::Reviewer),
251 "custom" => Ok(Self::Custom),
252 other => Err(anyhow::anyhow!("unknown worker agent kind '{}'", other)),
253 }
254 }
255}
256
257pub type CattleAgentKind = WorkerAgentKind;
259pub type CattleAgentSpec = WorkerAgentSpec;
261
262impl WorkerAgentSpec {
263 pub fn new(
265 kind: WorkerAgentKind,
266 name: impl Into<String>,
267 description: impl Into<String>,
268 ) -> Self {
269 Self {
270 name: name.into(),
271 description: description.into(),
272 kind,
273 hidden: false,
274 permissions: None,
275 model: None,
276 prompt: None,
277 max_steps: None,
278 confirmation_inheritance: None,
279 }
280 }
281
282 pub fn read_only(name: impl Into<String>, description: impl Into<String>) -> Self {
284 Self::new(WorkerAgentKind::ReadOnly, name, description)
285 }
286
287 pub fn planner(name: impl Into<String>, description: impl Into<String>) -> Self {
289 Self::new(WorkerAgentKind::Planner, name, description)
290 }
291
292 pub fn implementer(name: impl Into<String>, description: impl Into<String>) -> Self {
294 Self::new(WorkerAgentKind::Implementer, name, description)
295 }
296
297 pub fn verifier(name: impl Into<String>, description: impl Into<String>) -> Self {
299 Self::new(WorkerAgentKind::Verifier, name, description)
300 }
301
302 pub fn reviewer(name: impl Into<String>, description: impl Into<String>) -> Self {
304 Self::new(WorkerAgentKind::Reviewer, name, description)
305 }
306
307 pub fn custom(name: impl Into<String>, description: impl Into<String>) -> Self {
309 Self::new(WorkerAgentKind::Custom, name, description)
310 }
311
312 pub fn hidden(mut self, hidden: bool) -> Self {
314 self.hidden = hidden;
315 self
316 }
317
318 pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
320 self.permissions = Some(permissions);
321 self
322 }
323
324 pub fn with_model(mut self, model: ModelConfig) -> Self {
326 self.model = Some(model);
327 self
328 }
329
330 pub fn with_model_ref(mut self, model_ref: impl AsRef<str>) -> Self {
332 self.model = Some(ModelConfig::from_model_ref(model_ref));
333 self
334 }
335
336 pub fn with_provider_model(
338 mut self,
339 provider: impl Into<String>,
340 model: impl Into<String>,
341 ) -> Self {
342 self.model = Some(ModelConfig::with_provider(provider, model));
343 self
344 }
345
346 pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
348 self.prompt = Some(prompt.into());
349 self
350 }
351
352 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
354 self.max_steps = Some(max_steps);
355 self
356 }
357
358 pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
360 self.confirmation_inheritance = Some(inheritance);
361 self
362 }
363
364 pub fn into_agent_definition(self) -> AgentDefinition {
366 let mut agent = AgentDefinition::new(&self.name, &self.description)
367 .with_permissions(
368 self.permissions
369 .unwrap_or_else(|| self.kind.default_permissions()),
370 )
371 .with_max_steps(
372 self.max_steps
373 .unwrap_or_else(|| self.kind.default_max_steps()),
374 );
375
376 if self.hidden {
377 agent = agent.hidden();
378 }
379 if let Some(model) = self.model {
380 agent = agent.with_model(model);
381 }
382 if let Some(prompt) = self
383 .prompt
384 .or_else(|| self.kind.default_prompt().map(str::to_string))
385 {
386 agent = agent.with_prompt(&prompt);
387 }
388 if let Some(ci) = self.confirmation_inheritance {
389 agent = agent.with_confirmation(ci);
390 }
391 agent
392 }
393}
394
395impl From<WorkerAgentSpec> for AgentDefinition {
396 fn from(spec: WorkerAgentSpec) -> Self {
397 spec.into_agent_definition()
398 }
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct AgentDefinition {
406 pub name: String,
408 pub description: String,
410 #[serde(default)]
412 pub native: bool,
413 #[serde(default)]
415 pub hidden: bool,
416 #[serde(default)]
418 pub permissions: PermissionPolicy,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub model: Option<ModelConfig>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub prompt: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub max_steps: Option<usize>,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub confirmation_inheritance: Option<ConfirmationInheritance>,
432}
433
434impl AgentDefinition {
435 pub fn new(name: &str, description: &str) -> Self {
437 Self {
438 name: name.to_string(),
439 description: description.to_string(),
440 native: false,
441 hidden: false,
442 permissions: PermissionPolicy::default(),
443 model: None,
444 prompt: None,
445 max_steps: None,
446 confirmation_inheritance: None,
447 }
448 }
449
450 pub fn worker(spec: WorkerAgentSpec) -> Self {
452 spec.into_agent_definition()
453 }
454
455 pub fn native(mut self) -> Self {
457 self.native = true;
458 self
459 }
460
461 pub fn hidden(mut self) -> Self {
463 self.hidden = true;
464 self
465 }
466
467 pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
469 self.permissions = permissions;
470 self
471 }
472
473 pub fn with_model(mut self, model: ModelConfig) -> Self {
475 self.model = Some(model);
476 self
477 }
478
479 pub fn with_prompt(mut self, prompt: &str) -> Self {
481 self.prompt = Some(prompt.to_string());
482 self
483 }
484
485 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
487 self.max_steps = Some(max_steps);
488 self
489 }
490
491 pub fn has_defined_permissions(&self) -> bool {
493 !self.permissions.allow.is_empty() || !self.permissions.deny.is_empty()
494 }
495
496 pub(crate) fn apply_to(&self, config: &mut crate::agent::AgentConfig) {
502 use std::sync::Arc;
503
504 if config.permission_checker.is_none() && self.has_defined_permissions() {
505 config.permission_checker =
506 Some(Arc::new(self.permissions.clone()) as Arc<dyn PermissionChecker>);
507 config.permission_policy = Some(self.permissions.clone());
508 }
509
510 if let Some(ref prompt) = self.prompt {
511 if config.prompt_slots.extra.is_none() {
512 config.prompt_slots.extra = Some(prompt.clone());
513 }
514 }
515
516 if let Some(max_steps) = self.max_steps {
517 if config.max_tool_rounds == crate::agent::MAX_TOOL_ROUNDS {
518 config.max_tool_rounds = max_steps;
519 }
520 }
521
522 if config.confirmation_manager.is_none() {
524 let inheritance = self.confirmation_inheritance.clone().unwrap_or_else(|| {
525 if self.has_defined_permissions() {
526 ConfirmationInheritance::AutoApprove
527 } else {
528 ConfirmationInheritance::DenyOnAsk
529 }
530 });
531 match inheritance {
532 ConfirmationInheritance::AutoApprove => {
533 config.confirmation_manager =
534 Some(Arc::new(crate::hitl::AutoApproveConfirmation));
535 }
536 ConfirmationInheritance::DenyOnAsk => { }
537 ConfirmationInheritance::InheritParent => { }
538 }
539 }
540 }
541
542 pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
544 self.confirmation_inheritance = Some(inheritance);
545 self
546 }
547}
548
549pub struct AgentRegistry {
554 agents: RwLock<HashMap<String, AgentDefinition>>,
555}
556
557fn canonical_agent_name(name: &str) -> &str {
558 match name.trim() {
559 "general-purpose" | "general_purpose" | "generalpurpose" => "general",
560 "deep_research" | "deepresearch" => "deep-research",
561 "verify" | "verifier" => "verification",
562 "code-review" | "code_reviewer" | "reviewer" => "review",
563 other => other,
564 }
565}
566
567impl Default for AgentRegistry {
568 fn default() -> Self {
569 Self::new()
570 }
571}
572
573impl AgentRegistry {
574 pub fn new() -> Self {
576 let registry = Self {
577 agents: RwLock::new(HashMap::new()),
578 };
579
580 for agent in builtin_agents() {
582 registry.register(agent);
583 }
584
585 registry
586 }
587
588 pub fn with_config(config: &CodeConfig) -> Self {
592 let registry = Self::new();
593
594 for dir in &config.agent_dirs {
596 let agents = load_agents_from_dir(dir);
597 for agent in agents {
598 tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
599 registry.register(agent);
600 }
601 }
602
603 registry
604 }
605
606 pub fn register(&self, agent: AgentDefinition) {
608 let mut agents = write_or_recover(&self.agents);
609 tracing::debug!("Registering agent: {}", agent.name);
610 agents.insert(agent.name.clone(), agent);
611 }
612
613 pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
618 let agent = spec.into_agent_definition();
619 self.register(agent.clone());
620 agent
621 }
622
623 pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
625 where
626 I: IntoIterator<Item = WorkerAgentSpec>,
627 {
628 specs
629 .into_iter()
630 .map(|spec| self.register_worker(spec))
631 .collect()
632 }
633
634 pub fn unregister(&self, name: &str) -> bool {
638 let mut agents = write_or_recover(&self.agents);
639 agents.remove(name).is_some()
640 }
641
642 pub fn get(&self, name: &str) -> Option<AgentDefinition> {
644 let agents = read_or_recover(&self.agents);
645 agents
646 .get(name)
647 .or_else(|| agents.get(canonical_agent_name(name)))
648 .cloned()
649 }
650
651 pub fn list(&self) -> Vec<AgentDefinition> {
653 let agents = read_or_recover(&self.agents);
654 agents.values().cloned().collect()
655 }
656
657 pub fn list_visible(&self) -> Vec<AgentDefinition> {
659 let agents = read_or_recover(&self.agents);
660 agents.values().filter(|a| !a.hidden).cloned().collect()
661 }
662
663 pub fn exists(&self, name: &str) -> bool {
665 let agents = read_or_recover(&self.agents);
666 agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
667 }
668
669 pub fn len(&self) -> usize {
671 let agents = read_or_recover(&self.agents);
672 agents.len()
673 }
674
675 pub fn is_empty(&self) -> bool {
677 self.len() == 0
678 }
679}
680
681#[path = "subagent/loader.rs"]
686mod loader;
687pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
688
689pub fn builtin_agents() -> Vec<AgentDefinition> {
691 vec![
692 AgentDefinition::new(
694 "explore",
695 "Fast read-only exploration agent. Use for searching files, reading code, \
696 understanding codebase structure, and gathering external web evidence.",
697 )
698 .native()
699 .with_permissions(explore_permissions())
700 .with_max_steps(20)
701 .with_prompt(EXPLORE_PROMPT),
702 AgentDefinition::new(
704 "general",
705 "General-purpose agent for multi-step task execution. Can read, write, \
706 and execute commands.",
707 )
708 .native()
709 .with_permissions(general_permissions())
710 .with_max_steps(50),
711 AgentDefinition::new(
714 "deep-research",
715 "DeepResearch evidence agent. Inherits the parent session's tools, \
716 skills, permissions, and HITL policy for bounded evidence collection.",
717 )
718 .native()
719 .hidden()
720 .with_confirmation(ConfirmationInheritance::InheritParent)
721 .with_max_steps(50),
722 AgentDefinition::new(
724 "plan",
725 "Planning agent for designing implementation approaches. Read-only access \
726 to explore codebase and create plans.",
727 )
728 .native()
729 .with_permissions(plan_permissions())
730 .with_max_steps(30)
731 .with_prompt(PLAN_PROMPT),
732 AgentDefinition::new(
734 "verification",
735 "Verification agent for adversarial validation. Prefer real checks, \
736 reproductions, and regression testing over code reading alone.",
737 )
738 .native()
739 .with_permissions(verification_permissions())
740 .with_max_steps(30)
741 .with_prompt(VERIFICATION_PROMPT),
742 AgentDefinition::new(
744 "review",
745 "Code review agent focused on correctness, regressions, security, \
746 maintainability, and clear findings.",
747 )
748 .native()
749 .with_permissions(review_permissions())
750 .with_max_steps(25)
751 .with_prompt(REVIEW_PROMPT),
752 ]
753}
754
755fn explore_permissions() -> PermissionPolicy {
761 let mut policy = PermissionPolicy::new()
762 .allow_all(&["read", "grep", "glob", "ls", "web_fetch", "web_search"])
763 .deny_all(&["write", "edit", "task", "parallel_task"])
764 .allow("Bash(ls:*)")
765 .allow("Bash(cat:*)")
766 .allow("Bash(head:*)")
767 .allow("Bash(tail:*)")
768 .allow("Bash(find:*)")
769 .allow("Bash(wc:*)")
770 .deny("Bash(rm:*)")
771 .deny("Bash(mv:*)")
772 .deny("Bash(cp:*)");
773 policy.default_decision = PermissionDecision::Deny;
774 policy
775}
776
777fn general_permissions() -> PermissionPolicy {
779 PermissionPolicy::new()
780 .allow_all(&[
781 "read",
782 "write",
783 "edit",
784 "grep",
785 "glob",
786 "ls",
787 "bash",
788 "web_fetch",
789 "web_search",
790 "git",
791 "patch",
792 "batch",
793 "generate_object",
794 ])
795 .deny("task")
796 .deny("parallel_task")
797}
798
799fn plan_permissions() -> PermissionPolicy {
801 let mut policy = PermissionPolicy::new()
802 .allow_all(&["read", "grep", "glob", "ls"])
803 .deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
804 policy.default_decision = PermissionDecision::Deny;
805 policy
806}
807
808fn verification_permissions() -> PermissionPolicy {
810 let mut policy = PermissionPolicy::new()
811 .allow_all(&[
812 "read",
813 "grep",
814 "glob",
815 "ls",
816 "bash",
817 "web_fetch",
818 "web_search",
819 ])
820 .deny_all(&["write", "edit", "task", "parallel_task"]);
821 policy.default_decision = PermissionDecision::Deny;
822 policy
823}
824
825fn review_permissions() -> PermissionPolicy {
827 let mut policy = PermissionPolicy::new()
828 .allow_all(&[
829 "read",
830 "grep",
831 "glob",
832 "ls",
833 "bash",
834 "web_fetch",
835 "web_search",
836 ])
837 .deny_all(&["write", "edit", "task", "parallel_task"]);
838 policy.default_decision = PermissionDecision::Deny;
839 policy
840}
841
842const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
847
848const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
849
850const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
851
852const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
853
854#[cfg(test)]
859#[path = "subagent/tests.rs"]
860mod tests;