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_inheritance.is_none() {
544 config.confirmation_inheritance =
545 Some(self.confirmation_inheritance.clone().unwrap_or_else(|| {
546 if self.has_defined_permissions() {
547 ConfirmationInheritance::AutoApprove
548 } else {
549 ConfirmationInheritance::DenyOnAsk
550 }
551 }));
552 }
553 if config.confirmation_manager.is_none() {
554 match config.confirmation_inheritance.as_ref() {
555 Some(ConfirmationInheritance::AutoApprove) => {
556 config.confirmation_manager =
557 Some(Arc::new(crate::hitl::AutoApproveConfirmation));
558 }
559 Some(ConfirmationInheritance::DenyOnAsk) | None => {
560 }
562 Some(ConfirmationInheritance::InheritParent) => {
563 }
565 }
566 }
567 }
568
569 pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
571 self.confirmation_inheritance = Some(inheritance);
572 self
573 }
574}
575
576pub struct AgentRegistry {
581 agents: RwLock<HashMap<String, AgentDefinition>>,
582}
583
584fn canonical_agent_name(name: &str) -> &str {
585 match name.trim() {
586 "general-purpose" | "general_purpose" | "generalpurpose" => "general",
587 "deep_research" | "deepresearch" => "deep-research",
588 "verify" | "verifier" => "verification",
589 "code-review" | "code_reviewer" | "reviewer" => "review",
590 other => other,
591 }
592}
593
594impl Default for AgentRegistry {
595 fn default() -> Self {
596 Self::new()
597 }
598}
599
600impl AgentRegistry {
601 pub fn new() -> Self {
603 let registry = Self {
604 agents: RwLock::new(HashMap::new()),
605 };
606
607 for agent in builtin_agents() {
609 registry.register(agent);
610 }
611
612 registry
613 }
614
615 pub fn with_config(config: &CodeConfig) -> Self {
619 let registry = Self::new();
620
621 for dir in &config.agent_dirs {
623 let agents = load_agents_from_dir(dir);
624 for agent in agents {
625 tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
626 registry.register(agent);
627 }
628 }
629
630 registry
631 }
632
633 pub fn register(&self, agent: AgentDefinition) {
635 let mut agents = write_or_recover(&self.agents);
636 tracing::debug!("Registering agent: {}", agent.name);
637 agents.insert(agent.name.clone(), agent);
638 }
639
640 pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
645 let agent = spec.into_agent_definition();
646 self.register(agent.clone());
647 agent
648 }
649
650 pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
652 where
653 I: IntoIterator<Item = WorkerAgentSpec>,
654 {
655 specs
656 .into_iter()
657 .map(|spec| self.register_worker(spec))
658 .collect()
659 }
660
661 pub fn unregister(&self, name: &str) -> bool {
665 let mut agents = write_or_recover(&self.agents);
666 agents.remove(name).is_some()
667 }
668
669 pub fn get(&self, name: &str) -> Option<AgentDefinition> {
671 let agents = read_or_recover(&self.agents);
672 agents
673 .get(name)
674 .or_else(|| agents.get(canonical_agent_name(name)))
675 .cloned()
676 }
677
678 pub fn list(&self) -> Vec<AgentDefinition> {
680 let agents = read_or_recover(&self.agents);
681 agents.values().cloned().collect()
682 }
683
684 pub fn list_visible(&self) -> Vec<AgentDefinition> {
686 let agents = read_or_recover(&self.agents);
687 agents.values().filter(|a| !a.hidden).cloned().collect()
688 }
689
690 pub fn exists(&self, name: &str) -> bool {
692 let agents = read_or_recover(&self.agents);
693 agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
694 }
695
696 pub fn len(&self) -> usize {
698 let agents = read_or_recover(&self.agents);
699 agents.len()
700 }
701
702 pub fn is_empty(&self) -> bool {
704 self.len() == 0
705 }
706}
707
708#[path = "subagent/loader.rs"]
713mod loader;
714pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
715
716pub fn builtin_agents() -> Vec<AgentDefinition> {
718 vec![
719 AgentDefinition::new(
721 "explore",
722 "Fast read-only exploration agent. Use for searching files, reading code, \
723 understanding codebase structure, and gathering external web evidence.",
724 )
725 .native()
726 .with_permissions(explore_permissions())
727 .with_max_steps(20)
728 .with_prompt(EXPLORE_PROMPT),
729 AgentDefinition::new(
731 "general",
732 "General-purpose agent for multi-step task execution. Can read, write, \
733 and execute commands.",
734 )
735 .native()
736 .with_permissions(general_permissions())
737 .with_max_steps(50),
738 AgentDefinition::new(
741 "deep-research",
742 "DeepResearch evidence agent. Inherits the parent session's tools, \
743 skills, permissions, and HITL policy for bounded evidence collection.",
744 )
745 .native()
746 .hidden()
747 .with_confirmation(ConfirmationInheritance::InheritParent)
748 .with_max_steps(50),
749 AgentDefinition::new(
752 "loop-planner",
753 "Tool-free semantic planner for engineered loops.",
754 )
755 .native()
756 .hidden()
757 .tool_free()
758 .with_max_steps(4)
759 .with_prompt(LOOP_PLANNER_PROMPT),
760 AgentDefinition::new(
761 "loop-checker",
762 "Tool-free independent checker for engineered loops.",
763 )
764 .native()
765 .hidden()
766 .tool_free()
767 .with_max_steps(4)
768 .with_prompt(LOOP_CHECKER_PROMPT),
769 AgentDefinition::new(
771 "plan",
772 "Planning agent for designing implementation approaches. Read-only access \
773 to explore codebase and create plans.",
774 )
775 .native()
776 .with_permissions(plan_permissions())
777 .with_max_steps(30)
778 .with_prompt(PLAN_PROMPT),
779 AgentDefinition::new(
781 "verification",
782 "Verification agent for adversarial validation. Prefer real checks, \
783 reproductions, and regression testing over code reading alone.",
784 )
785 .native()
786 .with_permissions(verification_permissions())
787 .with_max_steps(30)
788 .with_prompt(VERIFICATION_PROMPT),
789 AgentDefinition::new(
791 "review",
792 "Code review agent focused on correctness, regressions, security, \
793 maintainability, and clear findings.",
794 )
795 .native()
796 .with_permissions(review_permissions())
797 .with_max_steps(25)
798 .with_prompt(REVIEW_PROMPT),
799 ]
800}
801
802fn explore_permissions() -> PermissionPolicy {
808 let mut policy = PermissionPolicy::new()
809 .allow_all(&["read", "grep", "glob", "ls", "web_fetch", "web_search"])
810 .deny_all(&["write", "edit", "task", "parallel_task"])
811 .allow("Bash(ls:*)")
812 .allow("Bash(cat:*)")
813 .allow("Bash(head:*)")
814 .allow("Bash(tail:*)")
815 .allow("Bash(find:*)")
816 .allow("Bash(wc:*)")
817 .deny("Bash(rm:*)")
818 .deny("Bash(mv:*)")
819 .deny("Bash(cp:*)");
820 policy.default_decision = PermissionDecision::Deny;
821 policy
822}
823
824fn general_permissions() -> PermissionPolicy {
826 PermissionPolicy::new()
827 .allow_all(&[
828 "read",
829 "write",
830 "edit",
831 "grep",
832 "glob",
833 "ls",
834 "bash",
835 "web_fetch",
836 "web_search",
837 "git",
838 "patch",
839 "batch",
840 "generate_object",
841 ])
842 .deny("task")
843 .deny("parallel_task")
844}
845
846fn plan_permissions() -> PermissionPolicy {
848 let mut policy = PermissionPolicy::new()
849 .allow_all(&["read", "grep", "glob", "ls"])
850 .deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
851 policy.default_decision = PermissionDecision::Deny;
852 policy
853}
854
855fn verification_permissions() -> PermissionPolicy {
857 let mut policy = PermissionPolicy::new()
858 .allow_all(&[
859 "read",
860 "grep",
861 "glob",
862 "ls",
863 "bash",
864 "web_fetch",
865 "web_search",
866 ])
867 .deny_all(&["write", "edit", "task", "parallel_task"]);
868 policy.default_decision = PermissionDecision::Deny;
869 policy
870}
871
872fn review_permissions() -> PermissionPolicy {
874 let mut policy = PermissionPolicy::new()
875 .allow_all(&[
876 "read",
877 "grep",
878 "glob",
879 "ls",
880 "bash",
881 "web_fetch",
882 "web_search",
883 ])
884 .deny_all(&["write", "edit", "task", "parallel_task"]);
885 policy.default_decision = PermissionDecision::Deny;
886 policy
887}
888
889const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
894
895const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
896
897const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
898
899const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
900
901const 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.";
902
903const 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.";
904
905#[cfg(test)]
910#[path = "subagent/tests.rs"]
911mod tests;