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)]
430 pub tool_free: bool,
431 #[serde(default, skip_serializing_if = "Option::is_none")]
434 pub confirmation_inheritance: Option<ConfirmationInheritance>,
435}
436
437impl AgentDefinition {
438 pub fn new(name: &str, description: &str) -> Self {
440 Self {
441 name: name.to_string(),
442 description: description.to_string(),
443 native: false,
444 hidden: false,
445 permissions: PermissionPolicy::default(),
446 model: None,
447 prompt: None,
448 max_steps: None,
449 tool_free: false,
450 confirmation_inheritance: None,
451 }
452 }
453
454 pub fn worker(spec: WorkerAgentSpec) -> Self {
456 spec.into_agent_definition()
457 }
458
459 pub fn native(mut self) -> Self {
461 self.native = true;
462 self
463 }
464
465 pub fn hidden(mut self) -> Self {
467 self.hidden = true;
468 self
469 }
470
471 pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
473 self.permissions = permissions;
474 self
475 }
476
477 pub fn with_model(mut self, model: ModelConfig) -> Self {
479 self.model = Some(model);
480 self
481 }
482
483 pub fn with_prompt(mut self, prompt: &str) -> Self {
485 self.prompt = Some(prompt.to_string());
486 self
487 }
488
489 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
491 self.max_steps = Some(max_steps);
492 self
493 }
494
495 pub fn tool_free(mut self) -> Self {
498 self.tool_free = true;
499 self
500 }
501
502 pub fn has_defined_permissions(&self) -> bool {
504 !self.permissions.allow.is_empty() || !self.permissions.deny.is_empty()
505 }
506
507 pub(crate) fn apply_to(&self, config: &mut crate::agent::AgentConfig) {
513 use std::sync::Arc;
514
515 if self.tool_free {
516 config.tools.clear();
517 let policy = PermissionPolicy::strict();
518 config.permission_checker = Some(Arc::new(policy.clone()));
519 config.permission_policy = Some(policy);
520 }
521
522 if !self.tool_free && config.permission_checker.is_none() && self.has_defined_permissions()
523 {
524 config.permission_checker =
525 Some(Arc::new(self.permissions.clone()) as Arc<dyn PermissionChecker>);
526 config.permission_policy = Some(self.permissions.clone());
527 }
528
529 if let Some(ref prompt) = self.prompt {
530 if config.prompt_slots.extra.is_none() {
531 config.prompt_slots.extra = Some(prompt.clone());
532 }
533 }
534
535 if let Some(max_steps) = self.max_steps {
536 if config.max_tool_rounds == crate::agent::MAX_TOOL_ROUNDS {
537 config.max_tool_rounds = max_steps;
538 }
539 }
540
541 if config.confirmation_manager.is_none() {
543 let inheritance = self.confirmation_inheritance.clone().unwrap_or_else(|| {
544 if self.has_defined_permissions() {
545 ConfirmationInheritance::AutoApprove
546 } else {
547 ConfirmationInheritance::DenyOnAsk
548 }
549 });
550 match inheritance {
551 ConfirmationInheritance::AutoApprove => {
552 config.confirmation_manager =
553 Some(Arc::new(crate::hitl::AutoApproveConfirmation));
554 }
555 ConfirmationInheritance::DenyOnAsk => { }
556 ConfirmationInheritance::InheritParent => { }
557 }
558 }
559 }
560
561 pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
563 self.confirmation_inheritance = Some(inheritance);
564 self
565 }
566}
567
568pub struct AgentRegistry {
573 agents: RwLock<HashMap<String, AgentDefinition>>,
574}
575
576fn canonical_agent_name(name: &str) -> &str {
577 match name.trim() {
578 "general-purpose" | "general_purpose" | "generalpurpose" => "general",
579 "deep_research" | "deepresearch" => "deep-research",
580 "verify" | "verifier" => "verification",
581 "code-review" | "code_reviewer" | "reviewer" => "review",
582 other => other,
583 }
584}
585
586impl Default for AgentRegistry {
587 fn default() -> Self {
588 Self::new()
589 }
590}
591
592impl AgentRegistry {
593 pub fn new() -> Self {
595 let registry = Self {
596 agents: RwLock::new(HashMap::new()),
597 };
598
599 for agent in builtin_agents() {
601 registry.register(agent);
602 }
603
604 registry
605 }
606
607 pub fn with_config(config: &CodeConfig) -> Self {
611 let registry = Self::new();
612
613 for dir in &config.agent_dirs {
615 let agents = load_agents_from_dir(dir);
616 for agent in agents {
617 tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
618 registry.register(agent);
619 }
620 }
621
622 registry
623 }
624
625 pub fn register(&self, agent: AgentDefinition) {
627 let mut agents = write_or_recover(&self.agents);
628 tracing::debug!("Registering agent: {}", agent.name);
629 agents.insert(agent.name.clone(), agent);
630 }
631
632 pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
637 let agent = spec.into_agent_definition();
638 self.register(agent.clone());
639 agent
640 }
641
642 pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
644 where
645 I: IntoIterator<Item = WorkerAgentSpec>,
646 {
647 specs
648 .into_iter()
649 .map(|spec| self.register_worker(spec))
650 .collect()
651 }
652
653 pub fn unregister(&self, name: &str) -> bool {
657 let mut agents = write_or_recover(&self.agents);
658 agents.remove(name).is_some()
659 }
660
661 pub fn get(&self, name: &str) -> Option<AgentDefinition> {
663 let agents = read_or_recover(&self.agents);
664 agents
665 .get(name)
666 .or_else(|| agents.get(canonical_agent_name(name)))
667 .cloned()
668 }
669
670 pub fn list(&self) -> Vec<AgentDefinition> {
672 let agents = read_or_recover(&self.agents);
673 agents.values().cloned().collect()
674 }
675
676 pub fn list_visible(&self) -> Vec<AgentDefinition> {
678 let agents = read_or_recover(&self.agents);
679 agents.values().filter(|a| !a.hidden).cloned().collect()
680 }
681
682 pub fn exists(&self, name: &str) -> bool {
684 let agents = read_or_recover(&self.agents);
685 agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
686 }
687
688 pub fn len(&self) -> usize {
690 let agents = read_or_recover(&self.agents);
691 agents.len()
692 }
693
694 pub fn is_empty(&self) -> bool {
696 self.len() == 0
697 }
698}
699
700#[path = "subagent/loader.rs"]
705mod loader;
706pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
707
708pub fn builtin_agents() -> Vec<AgentDefinition> {
710 vec![
711 AgentDefinition::new(
713 "explore",
714 "Fast read-only exploration agent. Use for searching files, reading code, \
715 understanding codebase structure, and gathering external web evidence.",
716 )
717 .native()
718 .with_permissions(explore_permissions())
719 .with_max_steps(20)
720 .with_prompt(EXPLORE_PROMPT),
721 AgentDefinition::new(
723 "general",
724 "General-purpose agent for multi-step task execution. Can read, write, \
725 and execute commands.",
726 )
727 .native()
728 .with_permissions(general_permissions())
729 .with_max_steps(50),
730 AgentDefinition::new(
733 "deep-research",
734 "DeepResearch evidence agent. Inherits the parent session's tools, \
735 skills, permissions, and HITL policy for bounded evidence collection.",
736 )
737 .native()
738 .hidden()
739 .with_confirmation(ConfirmationInheritance::InheritParent)
740 .with_max_steps(50),
741 AgentDefinition::new(
744 "loop-planner",
745 "Tool-free semantic planner for engineered loops.",
746 )
747 .native()
748 .hidden()
749 .tool_free()
750 .with_max_steps(4)
751 .with_prompt(LOOP_PLANNER_PROMPT),
752 AgentDefinition::new(
753 "loop-checker",
754 "Tool-free independent checker for engineered loops.",
755 )
756 .native()
757 .hidden()
758 .tool_free()
759 .with_max_steps(4)
760 .with_prompt(LOOP_CHECKER_PROMPT),
761 AgentDefinition::new(
763 "plan",
764 "Planning agent for designing implementation approaches. Read-only access \
765 to explore codebase and create plans.",
766 )
767 .native()
768 .with_permissions(plan_permissions())
769 .with_max_steps(30)
770 .with_prompt(PLAN_PROMPT),
771 AgentDefinition::new(
773 "verification",
774 "Verification agent for adversarial validation. Prefer real checks, \
775 reproductions, and regression testing over code reading alone.",
776 )
777 .native()
778 .with_permissions(verification_permissions())
779 .with_max_steps(30)
780 .with_prompt(VERIFICATION_PROMPT),
781 AgentDefinition::new(
783 "review",
784 "Code review agent focused on correctness, regressions, security, \
785 maintainability, and clear findings.",
786 )
787 .native()
788 .with_permissions(review_permissions())
789 .with_max_steps(25)
790 .with_prompt(REVIEW_PROMPT),
791 ]
792}
793
794fn explore_permissions() -> PermissionPolicy {
800 let mut policy = PermissionPolicy::new()
801 .allow_all(&["read", "grep", "glob", "ls", "web_fetch", "web_search"])
802 .deny_all(&["write", "edit", "task", "parallel_task"])
803 .allow("Bash(ls:*)")
804 .allow("Bash(cat:*)")
805 .allow("Bash(head:*)")
806 .allow("Bash(tail:*)")
807 .allow("Bash(find:*)")
808 .allow("Bash(wc:*)")
809 .deny("Bash(rm:*)")
810 .deny("Bash(mv:*)")
811 .deny("Bash(cp:*)");
812 policy.default_decision = PermissionDecision::Deny;
813 policy
814}
815
816fn general_permissions() -> PermissionPolicy {
818 PermissionPolicy::new()
819 .allow_all(&[
820 "read",
821 "write",
822 "edit",
823 "grep",
824 "glob",
825 "ls",
826 "bash",
827 "web_fetch",
828 "web_search",
829 "git",
830 "patch",
831 "batch",
832 "generate_object",
833 ])
834 .deny("task")
835 .deny("parallel_task")
836}
837
838fn plan_permissions() -> PermissionPolicy {
840 let mut policy = PermissionPolicy::new()
841 .allow_all(&["read", "grep", "glob", "ls"])
842 .deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
843 policy.default_decision = PermissionDecision::Deny;
844 policy
845}
846
847fn verification_permissions() -> PermissionPolicy {
849 let mut policy = PermissionPolicy::new()
850 .allow_all(&[
851 "read",
852 "grep",
853 "glob",
854 "ls",
855 "bash",
856 "web_fetch",
857 "web_search",
858 ])
859 .deny_all(&["write", "edit", "task", "parallel_task"]);
860 policy.default_decision = PermissionDecision::Deny;
861 policy
862}
863
864fn review_permissions() -> PermissionPolicy {
866 let mut policy = PermissionPolicy::new()
867 .allow_all(&[
868 "read",
869 "grep",
870 "glob",
871 "ls",
872 "bash",
873 "web_fetch",
874 "web_search",
875 ])
876 .deny_all(&["write", "edit", "task", "parallel_task"]);
877 policy.default_decision = PermissionDecision::Deny;
878 policy
879}
880
881const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
886
887const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
888
889const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
890
891const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
892
893const 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.";
894
895const 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.";
896
897#[cfg(test)]
902#[path = "subagent/tests.rs"]
903mod tests;