1use crate::config::CodeConfig;
59use crate::permissions::{PermissionChecker, PermissionDecision, PermissionPolicy};
60use serde::{Deserialize, Serialize};
61use std::collections::{HashMap, HashSet};
62use std::path::Path;
63use std::sync::{Arc, 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, Arc<AgentDefinition>>>,
582}
583
584#[derive(Debug, thiserror::Error)]
585#[error("projected agent name '{name}' conflicts with the compatibility registry")]
586pub(crate) struct AgentRegistrySnapshotError {
587 name: String,
588}
589
590impl AgentRegistrySnapshotError {
591 pub(crate) fn name(&self) -> &str {
592 &self.name
593 }
594}
595
596fn canonical_agent_name(name: &str) -> &str {
597 match name.trim() {
598 "general-purpose" | "general_purpose" | "generalpurpose" => "general",
599 "deep_research" | "deepresearch" => "deep-research",
600 "verify" | "verifier" => "verification",
601 "code-review" | "code_reviewer" | "reviewer" => "review",
602 other => other,
603 }
604}
605
606pub(crate) fn agent_names_conflict(left: &str, right: &str) -> bool {
607 canonical_agent_name(left) == canonical_agent_name(right)
608}
609
610impl Default for AgentRegistry {
611 fn default() -> Self {
612 Self::new()
613 }
614}
615
616impl AgentRegistry {
617 pub fn new() -> Self {
619 let registry = Self {
620 agents: RwLock::new(HashMap::new()),
621 };
622
623 for agent in builtin_agents() {
625 registry.register(agent);
626 }
627
628 registry
629 }
630
631 pub fn with_config(config: &CodeConfig) -> Self {
635 let registry = Self::new();
636
637 for dir in &config.agent_dirs {
639 let agents = load_agents_from_dir(dir);
640 for agent in agents {
641 tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
642 registry.register(agent);
643 }
644 }
645
646 registry
647 }
648
649 pub(crate) fn snapshot_with_external_agents(
657 &self,
658 external: impl IntoIterator<Item = Arc<AgentDefinition>>,
659 ) -> Result<Self, AgentRegistrySnapshotError> {
660 let mut agents = read_or_recover(&self.agents).clone();
661 let mut occupied = agents
662 .keys()
663 .map(|name| canonical_agent_name(name).to_owned())
664 .collect::<HashSet<_>>();
665 for agent in external {
666 let name = agent.name.clone();
667 if !occupied.insert(canonical_agent_name(&name).to_owned()) {
668 return Err(AgentRegistrySnapshotError { name });
669 }
670 agents.insert(name, agent);
671 }
672 Ok(Self {
673 agents: RwLock::new(agents),
674 })
675 }
676
677 pub fn register(&self, agent: AgentDefinition) {
679 self.register_arc(Arc::new(agent));
680 }
681
682 fn register_arc(&self, agent: Arc<AgentDefinition>) {
683 let mut agents = write_or_recover(&self.agents);
684 tracing::debug!("Registering agent: {}", agent.name);
685 agents.insert(agent.name.clone(), agent);
686 }
687
688 pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
693 let agent = spec.into_agent_definition();
694 self.register(agent.clone());
695 agent
696 }
697
698 pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
700 where
701 I: IntoIterator<Item = WorkerAgentSpec>,
702 {
703 specs
704 .into_iter()
705 .map(|spec| self.register_worker(spec))
706 .collect()
707 }
708
709 pub fn unregister(&self, name: &str) -> bool {
713 let mut agents = write_or_recover(&self.agents);
714 agents.remove(name).is_some()
715 }
716
717 pub fn get(&self, name: &str) -> Option<AgentDefinition> {
719 self.get_arc(name).map(|agent| agent.as_ref().clone())
720 }
721
722 pub(crate) fn get_arc(&self, name: &str) -> Option<Arc<AgentDefinition>> {
724 let agents = read_or_recover(&self.agents);
725 agents
726 .get(name)
727 .or_else(|| agents.get(canonical_agent_name(name)))
728 .cloned()
729 }
730
731 pub fn list(&self) -> Vec<AgentDefinition> {
733 let agents = read_or_recover(&self.agents);
734 agents
735 .values()
736 .map(|agent| agent.as_ref().clone())
737 .collect()
738 }
739
740 pub fn list_visible(&self) -> Vec<AgentDefinition> {
742 let agents = read_or_recover(&self.agents);
743 agents
744 .values()
745 .filter(|agent| !agent.hidden)
746 .map(|agent| agent.as_ref().clone())
747 .collect()
748 }
749
750 pub fn exists(&self, name: &str) -> bool {
752 let agents = read_or_recover(&self.agents);
753 agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
754 }
755
756 pub fn len(&self) -> usize {
758 let agents = read_or_recover(&self.agents);
759 agents.len()
760 }
761
762 pub fn is_empty(&self) -> bool {
764 self.len() == 0
765 }
766}
767
768#[path = "subagent/loader.rs"]
773mod loader;
774pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
775
776pub fn builtin_agents() -> Vec<AgentDefinition> {
778 vec![
779 AgentDefinition::new(
781 "explore",
782 "Fast read-only exploration agent. Use for searching files, reading code, \
783 understanding codebase structure, and gathering external web evidence.",
784 )
785 .native()
786 .with_permissions(explore_permissions())
787 .with_max_steps(20)
788 .with_prompt(EXPLORE_PROMPT),
789 AgentDefinition::new(
791 "general",
792 "General-purpose agent for multi-step task execution. Can read, write, \
793 and execute commands.",
794 )
795 .native()
796 .with_permissions(general_permissions())
797 .with_max_steps(50),
798 AgentDefinition::new(
801 "deep-research",
802 "DeepResearch evidence agent. Inherits the parent session's tools, \
803 skills, permissions, and HITL policy for bounded evidence collection.",
804 )
805 .native()
806 .hidden()
807 .with_confirmation(ConfirmationInheritance::InheritParent)
808 .with_max_steps(50),
809 AgentDefinition::new(
812 "loop-planner",
813 "Tool-free semantic planner for engineered loops.",
814 )
815 .native()
816 .hidden()
817 .tool_free()
818 .with_max_steps(4)
819 .with_prompt(LOOP_PLANNER_PROMPT),
820 AgentDefinition::new(
821 "loop-checker",
822 "Tool-free independent checker for engineered loops.",
823 )
824 .native()
825 .hidden()
826 .tool_free()
827 .with_max_steps(4)
828 .with_prompt(LOOP_CHECKER_PROMPT),
829 AgentDefinition::new(
831 "plan",
832 "Planning agent for designing implementation approaches. Read-only access \
833 to explore codebase and create plans.",
834 )
835 .native()
836 .with_permissions(plan_permissions())
837 .with_max_steps(30)
838 .with_prompt(PLAN_PROMPT),
839 AgentDefinition::new(
841 "verification",
842 "Verification agent for adversarial validation. Prefer real checks, \
843 reproductions, and regression testing over code reading alone.",
844 )
845 .native()
846 .with_permissions(verification_permissions())
847 .with_max_steps(30)
848 .with_prompt(VERIFICATION_PROMPT),
849 AgentDefinition::new(
851 "review",
852 "Code review agent focused on correctness, regressions, security, \
853 maintainability, and clear findings.",
854 )
855 .native()
856 .with_permissions(review_permissions())
857 .with_max_steps(25)
858 .with_prompt(REVIEW_PROMPT),
859 ]
860}
861
862fn explore_permissions() -> PermissionPolicy {
868 let mut policy = PermissionPolicy::new()
869 .allow_all(&["read", "search", "ls", "web_fetch", "web_search"])
870 .deny_all(&["write", "edit", "download", "task", "parallel_task"])
871 .allow("Bash(ls:*)")
872 .allow("Bash(cat:*)")
873 .allow("Bash(head:*)")
874 .allow("Bash(tail:*)")
875 .allow("Bash(find:*)")
876 .allow("Bash(wc:*)")
877 .deny("Bash(rm:*)")
878 .deny("Bash(mv:*)")
879 .deny("Bash(cp:*)");
880 policy.default_decision = PermissionDecision::Deny;
881 policy
882}
883
884fn general_permissions() -> PermissionPolicy {
886 PermissionPolicy::new()
887 .allow_all(&[
888 "read",
889 "write",
890 "edit",
891 "search",
892 "ls",
893 "bash",
894 "web_fetch",
895 "web_search",
896 "download",
897 "git",
898 "patch",
899 "batch",
900 "generate_object",
901 ])
902 .deny("task")
903 .deny("parallel_task")
904}
905
906fn plan_permissions() -> PermissionPolicy {
908 let mut policy = PermissionPolicy::new()
909 .allow_all(&["read", "search", "ls"])
910 .deny_all(&["write", "edit", "download", "bash", "task", "parallel_task"]);
911 policy.default_decision = PermissionDecision::Deny;
912 policy
913}
914
915fn verification_permissions() -> PermissionPolicy {
917 let mut policy = PermissionPolicy::new()
918 .allow_all(&["read", "search", "ls", "bash", "web_fetch", "web_search"])
919 .deny_all(&["write", "edit", "download", "task", "parallel_task"]);
920 policy.default_decision = PermissionDecision::Deny;
921 policy
922}
923
924fn review_permissions() -> PermissionPolicy {
926 let mut policy = PermissionPolicy::new()
927 .allow_all(&["read", "search", "ls", "bash", "web_fetch", "web_search"])
928 .deny_all(&["write", "edit", "download", "task", "parallel_task"]);
929 policy.default_decision = PermissionDecision::Deny;
930 policy
931}
932
933const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
938
939const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
940
941const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
942
943const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
944
945const 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.";
946
947const 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.";
948
949#[cfg(test)]
954#[path = "subagent/tests.rs"]
955mod tests;