Skip to main content

a3s_code_core/
subagent.rs

1//! Delegated Agent System
2//!
3//! Provides a system for delegating specialized tasks to focused child agents.
4//! Each delegated child run uses an isolated child session with restricted permissions.
5//!
6//! ## Architecture
7//!
8//! ```text
9//! Parent Session
10//!   └── Task Tool
11//!         ├── AgentRegistry (lookup agent definitions)
12//!         └── Child Session (isolated execution)
13//!               ├── Restricted permissions
14//!               ├── Optional model override
15//!               └── Event forwarding to parent
16//! ```
17//!
18//! ## Built-in Agents
19//!
20//! - `explore`: Fast codebase exploration (read-only)
21//! - `general`: Multi-step task execution
22//! - `deep-research`: Parent-policy evidence collection (hidden)
23//! - `plan`: Read-only planning mode
24//! - `verification`: Adversarial verification specialist
25//! - `review`: Code review specialist
26//!
27//! ## Loading Agents from Files
28//!
29//! Agents can be loaded from YAML or Markdown files:
30//!
31//! ### YAML Format
32//! ```yaml
33//! name: my-agent
34//! description: Custom agent for specific tasks
35//! hidden: false
36//! max_steps: 30
37//! permissions:
38//!   allow:
39//!     - read
40//!     - grep
41//!   deny:
42//!     - write
43//! prompt: |
44//!   You are a specialized agent...
45//! ```
46//!
47//! ### Markdown Format
48//! ```markdown
49//! ---
50//! name: my-agent
51//! description: Custom agent
52//! max_steps: 30
53//! ---
54//! # System Prompt
55//! You are a specialized agent...
56//! ```
57
58use 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/// How a child run resolves tools that require confirmation (PermissionDecision::Ask).
68#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
69#[serde(rename_all = "snake_case")]
70pub enum ConfirmationInheritance {
71    /// Auto-approve all Ask decisions. Safe when the agent has explicit allow-list
72    /// permissions that already define the access boundary.
73    #[default]
74    AutoApprove,
75    /// Deny all Ask decisions (strict mode — only explicitly allowed tools run).
76    DenyOnAsk,
77    /// Inherit the parent session's confirmation manager.
78    InheritParent,
79}
80
81/// Model configuration for agent.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ModelConfig {
84    /// Model identifier (e.g., "claude-3-5-sonnet-20241022")
85    pub model: String,
86    /// Optional provider override
87    pub provider: Option<String>,
88}
89
90impl ModelConfig {
91    /// Create a model override that inherits the default provider.
92    pub fn new(model: impl Into<String>) -> Self {
93        Self {
94            model: model.into(),
95            provider: None,
96        }
97    }
98
99    /// Create a model override from provider and model parts.
100    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    /// Parse a conventional `provider/model` reference.
108    ///
109    /// If no slash is present, the provider stays unset and the session binding
110    /// falls back to the host agent's default provider behavior.
111    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    /// Return the model as `provider/model` when a provider is set.
121    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/// Cattle-style worker agent role.
130///
131/// A worker role is a reproducible preset for disposable, task-scoped agents.
132/// Use [`WorkerAgentSpec`] to create many consistent workers instead of hand-tuning
133/// unique "pet" agents one by one.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum WorkerAgentKind {
137    /// Read-only exploration: fast code search and inspection.
138    #[serde(alias = "readonly", alias = "read-only", alias = "explore")]
139    ReadOnly,
140    /// Read-only planning: design work without modifying the workspace.
141    #[serde(alias = "plan")]
142    Planner,
143    /// Implementation work: read, edit, write, and run commands; no recursive task spawning.
144    #[serde(alias = "implementation", alias = "general")]
145    Implementer,
146    /// Verification work: run checks and inspect failures without editing files.
147    #[serde(alias = "verification", alias = "verify")]
148    Verifier,
149    /// Review work: inspect changes and report findings.
150    #[serde(alias = "review", alias = "code-review")]
151    Reviewer,
152    /// Strict custom worker: asks for any unspecified tool until permissions are supplied.
153    Custom,
154}
155
156/// Reproducible recipe for a disposable worker/subagent.
157///
158/// This is the public "cattle mode" API: callers define a small, serializable
159/// worker spec and register/spawn it repeatedly. The spec compiles to an
160/// [`AgentDefinition`] consumed by the existing delegation/runtime pipeline.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct WorkerAgentSpec {
163    /// Stable worker name used in task delegation (e.g., "frontend-fixer").
164    pub name: String,
165    /// Human-readable purpose shown to users and model selectors.
166    pub description: String,
167    /// Preset permission/prompt/step profile.
168    pub kind: WorkerAgentKind,
169    /// Hide from UI lists while still allowing explicit delegation.
170    #[serde(default)]
171    pub hidden: bool,
172    /// Optional permission policy override.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub permissions: Option<PermissionPolicy>,
175    /// Optional model override.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub model: Option<ModelConfig>,
178    /// Optional worker-specific prompt appended to the core agentic prompt.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub prompt: Option<String>,
181    /// Maximum execution steps/tool rounds.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub max_steps: Option<usize>,
184    /// How child runs resolve Ask decisions.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub confirmation_inheritance: Option<ConfirmationInheritance>,
187}
188
189impl WorkerAgentKind {
190    /// Stable snake_case identifier for this role.
191    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
257/// Backward-friendly alias for callers that name this pattern cattle mode.
258pub type CattleAgentKind = WorkerAgentKind;
259/// Backward-friendly alias for callers that name this pattern cattle mode.
260pub type CattleAgentSpec = WorkerAgentSpec;
261
262impl WorkerAgentSpec {
263    /// Create a worker spec from an explicit preset.
264    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    /// Read-only exploration worker.
283    pub fn read_only(name: impl Into<String>, description: impl Into<String>) -> Self {
284        Self::new(WorkerAgentKind::ReadOnly, name, description)
285    }
286
287    /// Read-only planning worker.
288    pub fn planner(name: impl Into<String>, description: impl Into<String>) -> Self {
289        Self::new(WorkerAgentKind::Planner, name, description)
290    }
291
292    /// Implementation worker with read/write/bash capability and no recursive task spawning.
293    pub fn implementer(name: impl Into<String>, description: impl Into<String>) -> Self {
294        Self::new(WorkerAgentKind::Implementer, name, description)
295    }
296
297    /// Verification worker for tests/checks/reproductions without edits.
298    pub fn verifier(name: impl Into<String>, description: impl Into<String>) -> Self {
299        Self::new(WorkerAgentKind::Verifier, name, description)
300    }
301
302    /// Review worker for correctness/regression/security findings.
303    pub fn reviewer(name: impl Into<String>, description: impl Into<String>) -> Self {
304        Self::new(WorkerAgentKind::Reviewer, name, description)
305    }
306
307    /// Strict custom worker. Provide permissions explicitly for non-HITL execution.
308    pub fn custom(name: impl Into<String>, description: impl Into<String>) -> Self {
309        Self::new(WorkerAgentKind::Custom, name, description)
310    }
311
312    /// Hide or show this worker in UI lists.
313    pub fn hidden(mut self, hidden: bool) -> Self {
314        self.hidden = hidden;
315        self
316    }
317
318    /// Override the preset permission policy.
319    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
320        self.permissions = Some(permissions);
321        self
322    }
323
324    /// Override the preset model.
325    pub fn with_model(mut self, model: ModelConfig) -> Self {
326        self.model = Some(model);
327        self
328    }
329
330    /// Override the preset model using `provider/model` or a model id.
331    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    /// Override the preset model using provider and model separately.
337    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    /// Override the preset prompt.
347    pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
348        self.prompt = Some(prompt.into());
349        self
350    }
351
352    /// Override the preset step budget.
353    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
354        self.max_steps = Some(max_steps);
355        self
356    }
357
358    /// Set confirmation inheritance policy for child runs.
359    pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
360        self.confirmation_inheritance = Some(inheritance);
361        self
362    }
363
364    /// Compile this worker recipe into a runtime agent definition.
365    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/// Agent definition
402///
403/// Defines the configuration and capabilities of an agent type.
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct AgentDefinition {
406    /// Agent identifier (e.g., "explore", "plan", "general")
407    pub name: String,
408    /// Description of what the agent does
409    pub description: String,
410    /// Whether this is a built-in agent
411    #[serde(default)]
412    pub native: bool,
413    /// Whether to hide from UI
414    #[serde(default)]
415    pub hidden: bool,
416    /// Permission rules for this agent
417    #[serde(default)]
418    pub permissions: PermissionPolicy,
419    /// Optional model override
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub model: Option<ModelConfig>,
422    /// System prompt for this agent
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub prompt: Option<String>,
425    /// Maximum execution steps (tool rounds)
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub max_steps: Option<usize>,
428    /// Whether this role is a pure model decision with no visible or executable tools.
429    #[serde(default)]
430    pub tool_free: bool,
431    /// How child runs resolve Ask decisions. Default: AutoApprove when
432    /// the agent has explicit allow rules, DenyOnAsk otherwise.
433    #[serde(default, skip_serializing_if = "Option::is_none")]
434    pub confirmation_inheritance: Option<ConfirmationInheritance>,
435}
436
437impl AgentDefinition {
438    /// Create a new agent definition
439    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    /// Create an agent definition from a disposable worker recipe.
455    pub fn worker(spec: WorkerAgentSpec) -> Self {
456        spec.into_agent_definition()
457    }
458
459    /// Mark as native (built-in)
460    pub fn native(mut self) -> Self {
461        self.native = true;
462        self
463    }
464
465    /// Mark as hidden from UI
466    pub fn hidden(mut self) -> Self {
467        self.hidden = true;
468        self
469    }
470
471    /// Set permission policy
472    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
473        self.permissions = permissions;
474        self
475    }
476
477    /// Set model override
478    pub fn with_model(mut self, model: ModelConfig) -> Self {
479        self.model = Some(model);
480        self
481    }
482
483    /// Set system prompt
484    pub fn with_prompt(mut self, prompt: &str) -> Self {
485        self.prompt = Some(prompt.to_string());
486        self
487    }
488
489    /// Set maximum execution steps
490    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
491        self.max_steps = Some(max_steps);
492        self
493    }
494
495    /// Make this role a pure LLM step. Tool definitions are removed before the
496    /// child turn, and parent tool permissions are not inherited into it.
497    pub fn tool_free(mut self) -> Self {
498        self.tool_free = true;
499        self
500    }
501
502    /// Whether this definition has non-empty permission rules.
503    pub fn has_defined_permissions(&self) -> bool {
504        !self.permissions.allow.is_empty() || !self.permissions.deny.is_empty()
505    }
506
507    /// Apply this definition's declared configuration to a mutable AgentConfig.
508    ///
509    /// Follows the "host overrides win" principle: only fills fields that are
510    /// currently at their default/None state. Callers who want to force values
511    /// should set them *after* calling `apply_to`.
512    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        // Confirmation inheritance: record how child-local Ask decisions are
542        // resolved before the parent boundary is composed into this run.
543        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                    /* leave None — safety_gate denies */
561                }
562                Some(ConfirmationInheritance::InheritParent) => {
563                    /* caller passes parent's manager */
564                }
565            }
566        }
567    }
568
569    /// Set confirmation inheritance policy for child runs.
570    pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
571        self.confirmation_inheritance = Some(inheritance);
572        self
573    }
574}
575
576/// Agent registry for managing agent definitions
577///
578/// Thread-safe registry that stores agent definitions and provides
579/// lookup functionality.
580pub 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    /// Create a new agent registry with built-in agents
618    pub fn new() -> Self {
619        let registry = Self {
620            agents: RwLock::new(HashMap::new()),
621        };
622
623        // Register built-in agents
624        for agent in builtin_agents() {
625            registry.register(agent);
626        }
627
628        registry
629    }
630
631    /// Create a new agent registry with configuration
632    ///
633    /// Loads built-in agents first, then loads agents from configured directories.
634    pub fn with_config(config: &CodeConfig) -> Self {
635        let registry = Self::new();
636
637        // Load agents from configured directories
638        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    /// Freeze the compatibility registry and merge one projected generation.
650    ///
651    /// The returned registry owns an independent name map, so later Session
652    /// registrations cannot rewrite an admitted Run, while immutable
653    /// definitions retain exact `Arc` identity. Exact names and the compatibility
654    /// aliases accepted by [`Self::get`] share one conflict domain and therefore
655    /// cannot shadow each other across the boundary.
656    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    /// Register an agent definition
678    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    /// Register a disposable worker agent from a reproducible spec.
689    ///
690    /// Returns the compiled [`AgentDefinition`] so callers can inspect or pass it
691    /// directly to `session_for_agent`.
692    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    /// Register multiple disposable worker agents and return their definitions.
699    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    /// Unregister an agent by name
710    ///
711    /// Returns true if the agent was removed, false if not found.
712    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    /// Get an agent definition by name
718    pub fn get(&self, name: &str) -> Option<AgentDefinition> {
719        self.get_arc(name).map(|agent| agent.as_ref().clone())
720    }
721
722    /// Resolve one definition without copying its prompt or permission data.
723    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    /// List all registered agents
732    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    /// List visible agents (not hidden)
741    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    /// Check if an agent exists
751    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    /// Get the number of registered agents
757    pub fn len(&self) -> usize {
758        let agents = read_or_recover(&self.agents);
759        agents.len()
760    }
761
762    /// Check if the registry is empty
763    pub fn is_empty(&self) -> bool {
764        self.len() == 0
765    }
766}
767
768// ============================================================================
769// Agent File Loading
770// ============================================================================
771
772#[path = "subagent/loader.rs"]
773mod loader;
774pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
775
776/// Create built-in agent definitions
777pub fn builtin_agents() -> Vec<AgentDefinition> {
778    vec![
779        // Explore agent: Fast codebase exploration (read-only)
780        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        // General agent: Multi-step task execution
790        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        // DeepResearch evidence agent: inherit the parent session's tool/skill
799        // policy instead of applying explore's read-only default-deny policy.
800        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        // Loop Engineering decision roles are intentionally tool-free. Makers
810        // gather evidence; planners and checkers only make structured decisions.
811        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        // Plan agent: Read-only planning mode
830        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        // Verification agent: adversarial validation and repro
840        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        // Review agent: review-focused analysis
850        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
862// ============================================================================
863// Permission Policies for Built-in Agents
864// ============================================================================
865
866/// Permission policy for explore agent (read-only)
867fn 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
884/// Permission policy for general agent (full access except task)
885fn 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
906/// Permission policy for plan agent (read-only)
907fn 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
915/// Permission policy for verification agent (read-heavy with runtime checks)
916fn 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
924/// Permission policy for review agent (read-heavy with optional lightweight checks)
925fn 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
933// ============================================================================
934// System Prompts for Built-in Agents
935// ============================================================================
936
937const 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// ============================================================================
950// Tests
951// ============================================================================
952
953#[cfg(test)]
954#[path = "subagent/tests.rs"]
955mod tests;