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//! - `plan`: Read-only planning mode
23//! - `verification`: Adversarial verification specialist
24//! - `review`: Code review specialist
25//!
26//! ## Loading Agents from Files
27//!
28//! Agents can be loaded from YAML or Markdown files:
29//!
30//! ### YAML Format
31//! ```yaml
32//! name: my-agent
33//! description: Custom agent for specific tasks
34//! hidden: false
35//! max_steps: 30
36//! permissions:
37//!   allow:
38//!     - read
39//!     - grep
40//!   deny:
41//!     - write
42//! prompt: |
43//!   You are a specialized agent...
44//! ```
45//!
46//! ### Markdown Format
47//! ```markdown
48//! ---
49//! name: my-agent
50//! description: Custom agent
51//! max_steps: 30
52//! ---
53//! # System Prompt
54//! You are a specialized agent...
55//! ```
56
57use crate::config::CodeConfig;
58use crate::permissions::PermissionPolicy;
59use serde::{Deserialize, Serialize};
60use std::collections::HashMap;
61use std::path::Path;
62use std::sync::RwLock;
63
64use crate::error::{read_or_recover, write_or_recover};
65
66/// Model configuration for agent.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ModelConfig {
69    /// Model identifier (e.g., "claude-3-5-sonnet-20241022")
70    pub model: String,
71    /// Optional provider override
72    pub provider: Option<String>,
73}
74
75impl ModelConfig {
76    /// Create a model override that inherits the default provider.
77    pub fn new(model: impl Into<String>) -> Self {
78        Self {
79            model: model.into(),
80            provider: None,
81        }
82    }
83
84    /// Create a model override from provider and model parts.
85    pub fn with_provider(provider: impl Into<String>, model: impl Into<String>) -> Self {
86        Self {
87            model: model.into(),
88            provider: Some(provider.into()),
89        }
90    }
91
92    /// Parse a conventional `provider/model` reference.
93    ///
94    /// If no slash is present, the provider stays unset and the session binding
95    /// falls back to the host agent's default provider behavior.
96    pub fn from_model_ref(model_ref: impl AsRef<str>) -> Self {
97        let model_ref = model_ref.as_ref();
98        if let Some((provider, model)) = model_ref.split_once('/') {
99            Self::with_provider(provider, model)
100        } else {
101            Self::new(model_ref)
102        }
103    }
104
105    /// Return the model as `provider/model` when a provider is set.
106    pub fn model_ref(&self) -> String {
107        match &self.provider {
108            Some(provider) => format!("{}/{}", provider, self.model),
109            None => self.model.clone(),
110        }
111    }
112}
113
114/// Cattle-style worker agent role.
115///
116/// A worker role is a reproducible preset for disposable, task-scoped agents.
117/// Use [`WorkerAgentSpec`] to create many consistent workers instead of hand-tuning
118/// unique "pet" agents one by one.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum WorkerAgentKind {
122    /// Read-only exploration: fast code search and inspection.
123    #[serde(alias = "readonly", alias = "read-only", alias = "explore")]
124    ReadOnly,
125    /// Read-only planning: design work without modifying the workspace.
126    #[serde(alias = "plan")]
127    Planner,
128    /// Implementation work: read, edit, write, and run commands; no recursive task spawning.
129    #[serde(alias = "implementation", alias = "general")]
130    Implementer,
131    /// Verification work: run checks and inspect failures without editing files.
132    #[serde(alias = "verification", alias = "verify")]
133    Verifier,
134    /// Review work: inspect changes and report findings.
135    #[serde(alias = "review", alias = "code-review")]
136    Reviewer,
137    /// Strict custom worker: asks for any unspecified tool until permissions are supplied.
138    Custom,
139}
140
141/// Reproducible recipe for a disposable worker/subagent.
142///
143/// This is the public "cattle mode" API: callers define a small, serializable
144/// worker spec and register/spawn it repeatedly. The spec compiles to an
145/// [`AgentDefinition`] consumed by the existing delegation/runtime pipeline.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct WorkerAgentSpec {
148    /// Stable worker name used in task delegation (e.g., "frontend-fixer").
149    pub name: String,
150    /// Human-readable purpose shown to users and model selectors.
151    pub description: String,
152    /// Preset permission/prompt/step profile.
153    pub kind: WorkerAgentKind,
154    /// Hide from UI lists while still allowing explicit delegation.
155    #[serde(default)]
156    pub hidden: bool,
157    /// Optional permission policy override.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub permissions: Option<PermissionPolicy>,
160    /// Optional model override.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub model: Option<ModelConfig>,
163    /// Optional worker-specific prompt appended to the core agentic prompt.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub prompt: Option<String>,
166    /// Maximum execution steps/tool rounds.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub max_steps: Option<usize>,
169}
170
171impl WorkerAgentKind {
172    /// Stable snake_case identifier for this role.
173    pub fn as_str(self) -> &'static str {
174        match self {
175            Self::ReadOnly => "read_only",
176            Self::Planner => "planner",
177            Self::Implementer => "implementer",
178            Self::Verifier => "verifier",
179            Self::Reviewer => "reviewer",
180            Self::Custom => "custom",
181        }
182    }
183
184    fn default_permissions(self) -> PermissionPolicy {
185        match self {
186            Self::ReadOnly => explore_permissions(),
187            Self::Planner => plan_permissions(),
188            Self::Implementer => general_permissions(),
189            Self::Verifier => verification_permissions(),
190            Self::Reviewer => review_permissions(),
191            Self::Custom => PermissionPolicy::strict(),
192        }
193    }
194
195    fn default_prompt(self) -> Option<&'static str> {
196        match self {
197            Self::ReadOnly => Some(EXPLORE_PROMPT),
198            Self::Planner => Some(PLAN_PROMPT),
199            Self::Verifier => Some(VERIFICATION_PROMPT),
200            Self::Reviewer => Some(REVIEW_PROMPT),
201            Self::Implementer | Self::Custom => None,
202        }
203    }
204
205    fn default_max_steps(self) -> usize {
206        match self {
207            Self::ReadOnly => 20,
208            Self::Planner => 30,
209            Self::Implementer => 50,
210            Self::Verifier => 30,
211            Self::Reviewer => 25,
212            Self::Custom => 30,
213        }
214    }
215}
216
217impl std::fmt::Display for WorkerAgentKind {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        f.write_str(self.as_str())
220    }
221}
222
223impl std::str::FromStr for WorkerAgentKind {
224    type Err = anyhow::Error;
225
226    fn from_str(value: &str) -> anyhow::Result<Self> {
227        match value.trim().to_ascii_lowercase().as_str() {
228            "read_only" | "readonly" | "read-only" | "explore" | "scanner" => Ok(Self::ReadOnly),
229            "planner" | "plan" => Ok(Self::Planner),
230            "implementer" | "implementation" | "general" | "executor" => Ok(Self::Implementer),
231            "verifier" | "verification" | "verify" | "tester" => Ok(Self::Verifier),
232            "reviewer" | "review" | "code-review" | "code_reviewer" => Ok(Self::Reviewer),
233            "custom" => Ok(Self::Custom),
234            other => Err(anyhow::anyhow!("unknown worker agent kind '{}'", other)),
235        }
236    }
237}
238
239/// Backward-friendly alias for callers that name this pattern cattle mode.
240pub type CattleAgentKind = WorkerAgentKind;
241/// Backward-friendly alias for callers that name this pattern cattle mode.
242pub type CattleAgentSpec = WorkerAgentSpec;
243
244impl WorkerAgentSpec {
245    /// Create a worker spec from an explicit preset.
246    pub fn new(
247        kind: WorkerAgentKind,
248        name: impl Into<String>,
249        description: impl Into<String>,
250    ) -> Self {
251        Self {
252            name: name.into(),
253            description: description.into(),
254            kind,
255            hidden: false,
256            permissions: None,
257            model: None,
258            prompt: None,
259            max_steps: None,
260        }
261    }
262
263    /// Read-only exploration worker.
264    pub fn read_only(name: impl Into<String>, description: impl Into<String>) -> Self {
265        Self::new(WorkerAgentKind::ReadOnly, name, description)
266    }
267
268    /// Read-only planning worker.
269    pub fn planner(name: impl Into<String>, description: impl Into<String>) -> Self {
270        Self::new(WorkerAgentKind::Planner, name, description)
271    }
272
273    /// Implementation worker with read/write/bash capability and no recursive task spawning.
274    pub fn implementer(name: impl Into<String>, description: impl Into<String>) -> Self {
275        Self::new(WorkerAgentKind::Implementer, name, description)
276    }
277
278    /// Verification worker for tests/checks/reproductions without edits.
279    pub fn verifier(name: impl Into<String>, description: impl Into<String>) -> Self {
280        Self::new(WorkerAgentKind::Verifier, name, description)
281    }
282
283    /// Review worker for correctness/regression/security findings.
284    pub fn reviewer(name: impl Into<String>, description: impl Into<String>) -> Self {
285        Self::new(WorkerAgentKind::Reviewer, name, description)
286    }
287
288    /// Strict custom worker. Provide permissions explicitly for non-HITL execution.
289    pub fn custom(name: impl Into<String>, description: impl Into<String>) -> Self {
290        Self::new(WorkerAgentKind::Custom, name, description)
291    }
292
293    /// Hide or show this worker in UI lists.
294    pub fn hidden(mut self, hidden: bool) -> Self {
295        self.hidden = hidden;
296        self
297    }
298
299    /// Override the preset permission policy.
300    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
301        self.permissions = Some(permissions);
302        self
303    }
304
305    /// Override the preset model.
306    pub fn with_model(mut self, model: ModelConfig) -> Self {
307        self.model = Some(model);
308        self
309    }
310
311    /// Override the preset model using `provider/model` or a model id.
312    pub fn with_model_ref(mut self, model_ref: impl AsRef<str>) -> Self {
313        self.model = Some(ModelConfig::from_model_ref(model_ref));
314        self
315    }
316
317    /// Override the preset model using provider and model separately.
318    pub fn with_provider_model(
319        mut self,
320        provider: impl Into<String>,
321        model: impl Into<String>,
322    ) -> Self {
323        self.model = Some(ModelConfig::with_provider(provider, model));
324        self
325    }
326
327    /// Override the preset prompt.
328    pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
329        self.prompt = Some(prompt.into());
330        self
331    }
332
333    /// Override the preset step budget.
334    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
335        self.max_steps = Some(max_steps);
336        self
337    }
338
339    /// Compile this worker recipe into a runtime agent definition.
340    pub fn into_agent_definition(self) -> AgentDefinition {
341        let mut agent = AgentDefinition::new(&self.name, &self.description)
342            .with_permissions(
343                self.permissions
344                    .unwrap_or_else(|| self.kind.default_permissions()),
345            )
346            .with_max_steps(
347                self.max_steps
348                    .unwrap_or_else(|| self.kind.default_max_steps()),
349            );
350
351        if self.hidden {
352            agent = agent.hidden();
353        }
354        if let Some(model) = self.model {
355            agent = agent.with_model(model);
356        }
357        if let Some(prompt) = self
358            .prompt
359            .or_else(|| self.kind.default_prompt().map(str::to_string))
360        {
361            agent = agent.with_prompt(&prompt);
362        }
363        agent
364    }
365}
366
367impl From<WorkerAgentSpec> for AgentDefinition {
368    fn from(spec: WorkerAgentSpec) -> Self {
369        spec.into_agent_definition()
370    }
371}
372
373/// Agent definition
374///
375/// Defines the configuration and capabilities of an agent type.
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct AgentDefinition {
378    /// Agent identifier (e.g., "explore", "plan", "general")
379    pub name: String,
380    /// Description of what the agent does
381    pub description: String,
382    /// Whether this is a built-in agent
383    #[serde(default)]
384    pub native: bool,
385    /// Whether to hide from UI
386    #[serde(default)]
387    pub hidden: bool,
388    /// Permission rules for this agent
389    #[serde(default)]
390    pub permissions: PermissionPolicy,
391    /// Optional model override
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub model: Option<ModelConfig>,
394    /// System prompt for this agent
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub prompt: Option<String>,
397    /// Maximum execution steps (tool rounds)
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub max_steps: Option<usize>,
400}
401
402impl AgentDefinition {
403    /// Create a new agent definition
404    pub fn new(name: &str, description: &str) -> Self {
405        Self {
406            name: name.to_string(),
407            description: description.to_string(),
408            native: false,
409            hidden: false,
410            permissions: PermissionPolicy::default(),
411            model: None,
412            prompt: None,
413            max_steps: None,
414        }
415    }
416
417    /// Create an agent definition from a disposable worker recipe.
418    pub fn worker(spec: WorkerAgentSpec) -> Self {
419        spec.into_agent_definition()
420    }
421
422    /// Mark as native (built-in)
423    pub fn native(mut self) -> Self {
424        self.native = true;
425        self
426    }
427
428    /// Mark as hidden from UI
429    pub fn hidden(mut self) -> Self {
430        self.hidden = true;
431        self
432    }
433
434    /// Set permission policy
435    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
436        self.permissions = permissions;
437        self
438    }
439
440    /// Set model override
441    pub fn with_model(mut self, model: ModelConfig) -> Self {
442        self.model = Some(model);
443        self
444    }
445
446    /// Set system prompt
447    pub fn with_prompt(mut self, prompt: &str) -> Self {
448        self.prompt = Some(prompt.to_string());
449        self
450    }
451
452    /// Set maximum execution steps
453    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
454        self.max_steps = Some(max_steps);
455        self
456    }
457}
458
459/// Agent registry for managing agent definitions
460///
461/// Thread-safe registry that stores agent definitions and provides
462/// lookup functionality.
463pub struct AgentRegistry {
464    agents: RwLock<HashMap<String, AgentDefinition>>,
465}
466
467impl Default for AgentRegistry {
468    fn default() -> Self {
469        Self::new()
470    }
471}
472
473impl AgentRegistry {
474    /// Create a new agent registry with built-in agents
475    pub fn new() -> Self {
476        let registry = Self {
477            agents: RwLock::new(HashMap::new()),
478        };
479
480        // Register built-in agents
481        for agent in builtin_agents() {
482            registry.register(agent);
483        }
484
485        registry
486    }
487
488    /// Create a new agent registry with configuration
489    ///
490    /// Loads built-in agents first, then loads agents from configured directories.
491    pub fn with_config(config: &CodeConfig) -> Self {
492        let registry = Self::new();
493
494        // Load agents from configured directories
495        for dir in &config.agent_dirs {
496            let agents = load_agents_from_dir(dir);
497            for agent in agents {
498                tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
499                registry.register(agent);
500            }
501        }
502
503        registry
504    }
505
506    /// Register an agent definition
507    pub fn register(&self, agent: AgentDefinition) {
508        let mut agents = write_or_recover(&self.agents);
509        tracing::debug!("Registering agent: {}", agent.name);
510        agents.insert(agent.name.clone(), agent);
511    }
512
513    /// Register a disposable worker agent from a reproducible spec.
514    ///
515    /// Returns the compiled [`AgentDefinition`] so callers can inspect or pass it
516    /// directly to `session_for_agent`.
517    pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
518        let agent = spec.into_agent_definition();
519        self.register(agent.clone());
520        agent
521    }
522
523    /// Register multiple disposable worker agents and return their definitions.
524    pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
525    where
526        I: IntoIterator<Item = WorkerAgentSpec>,
527    {
528        specs
529            .into_iter()
530            .map(|spec| self.register_worker(spec))
531            .collect()
532    }
533
534    /// Unregister an agent by name
535    ///
536    /// Returns true if the agent was removed, false if not found.
537    pub fn unregister(&self, name: &str) -> bool {
538        let mut agents = write_or_recover(&self.agents);
539        agents.remove(name).is_some()
540    }
541
542    /// Get an agent definition by name
543    pub fn get(&self, name: &str) -> Option<AgentDefinition> {
544        let agents = read_or_recover(&self.agents);
545        agents.get(name).cloned()
546    }
547
548    /// List all registered agents
549    pub fn list(&self) -> Vec<AgentDefinition> {
550        let agents = read_or_recover(&self.agents);
551        agents.values().cloned().collect()
552    }
553
554    /// List visible agents (not hidden)
555    pub fn list_visible(&self) -> Vec<AgentDefinition> {
556        let agents = read_or_recover(&self.agents);
557        agents.values().filter(|a| !a.hidden).cloned().collect()
558    }
559
560    /// Check if an agent exists
561    pub fn exists(&self, name: &str) -> bool {
562        let agents = read_or_recover(&self.agents);
563        agents.contains_key(name)
564    }
565
566    /// Get the number of registered agents
567    pub fn len(&self) -> usize {
568        let agents = read_or_recover(&self.agents);
569        agents.len()
570    }
571
572    /// Check if the registry is empty
573    pub fn is_empty(&self) -> bool {
574        self.len() == 0
575    }
576}
577
578// ============================================================================
579// Agent File Loading
580// ============================================================================
581
582/// Parse an agent definition from YAML content.
583///
584/// The YAML can describe either a full [`AgentDefinition`] or a cattle-style
585/// [`WorkerAgentSpec`] by including a `kind` field.
586pub fn parse_agent_yaml(content: &str) -> anyhow::Result<AgentDefinition> {
587    let value: serde_yaml::Value = serde_yaml::from_str(content)
588        .map_err(|e| anyhow::anyhow!("Failed to parse agent YAML: {}", e))?;
589
590    parse_agent_yaml_value(value, "agent YAML")
591}
592
593fn parse_agent_yaml_value(
594    value: serde_yaml::Value,
595    context: &str,
596) -> anyhow::Result<AgentDefinition> {
597    if yaml_value_has_key(&value, "kind") {
598        let spec: WorkerAgentSpec = serde_yaml::from_value(value)
599            .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
600        validate_agent_name(&spec.name)?;
601        return Ok(spec.into_agent_definition());
602    }
603
604    let agent: AgentDefinition = serde_yaml::from_value(value)
605        .map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", context, e))?;
606    validate_agent_name(&agent.name)?;
607    Ok(agent)
608}
609
610fn parse_worker_yaml_value(
611    value: serde_yaml::Value,
612    context: &str,
613) -> anyhow::Result<WorkerAgentSpec> {
614    let spec: WorkerAgentSpec = serde_yaml::from_value(value)
615        .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
616    validate_agent_name(&spec.name)?;
617    Ok(spec)
618}
619
620fn yaml_value_has_key(value: &serde_yaml::Value, key: &str) -> bool {
621    value
622        .as_mapping()
623        .map(|mapping| mapping.contains_key(serde_yaml::Value::String(key.to_string())))
624        .unwrap_or(false)
625}
626
627fn validate_agent_name(name: &str) -> anyhow::Result<()> {
628    if name.trim().is_empty() {
629        return Err(anyhow::anyhow!("Agent name is required"));
630    }
631    Ok(())
632}
633
634/// Parse an agent definition from Markdown with YAML frontmatter
635///
636/// The frontmatter contains agent metadata, and the body becomes the prompt.
637pub fn parse_agent_md(content: &str) -> anyhow::Result<AgentDefinition> {
638    // Parse frontmatter (YAML between --- markers)
639    let parts: Vec<&str> = content.splitn(3, "---").collect();
640
641    if parts.len() < 3 {
642        return Err(anyhow::anyhow!(
643            "Invalid markdown format: missing YAML frontmatter"
644        ));
645    }
646
647    let frontmatter = parts[1].trim();
648    let body = parts[2].trim();
649
650    // Parse the frontmatter as YAML. A `kind` field selects WorkerAgentSpec.
651    let value: serde_yaml::Value = serde_yaml::from_str(frontmatter)
652        .map_err(|e| anyhow::anyhow!("Failed to parse agent frontmatter: {}", e))?;
653
654    if yaml_value_has_key(&value, "kind") {
655        let mut spec = parse_worker_yaml_value(value, "frontmatter")?;
656        if spec.prompt.is_none() && !body.is_empty() {
657            spec.prompt = Some(body.to_string());
658        }
659        return Ok(spec.into_agent_definition());
660    }
661
662    let mut agent = parse_agent_yaml_value(value, "agent frontmatter")?;
663
664    // Use body as prompt if not already set in frontmatter.
665    if agent.prompt.is_none() && !body.is_empty() {
666        agent.prompt = Some(body.to_string());
667    }
668
669    Ok(agent)
670}
671
672/// Load all agent definitions from a directory
673///
674/// Scans for *.yaml and *.md files and parses them as agent definitions.
675/// Invalid files are logged and skipped.
676pub fn load_agents_from_dir(dir: &Path) -> Vec<AgentDefinition> {
677    let mut agents = Vec::new();
678
679    let Ok(entries) = std::fs::read_dir(dir) else {
680        tracing::warn!("Failed to read agent directory: {}", dir.display());
681        return agents;
682    };
683
684    for entry in entries.flatten() {
685        let path = entry.path();
686
687        // Skip non-files
688        if !path.is_file() {
689            continue;
690        }
691
692        let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
693            continue;
694        };
695
696        // Read file content
697        let Ok(content) = std::fs::read_to_string(&path) else {
698            tracing::warn!("Failed to read agent file: {}", path.display());
699            continue;
700        };
701
702        // Parse based on extension
703        let result = match ext {
704            "yaml" | "yml" => parse_agent_yaml(&content),
705            "md" => parse_agent_md(&content),
706            _ => continue,
707        };
708
709        match result {
710            Ok(agent) => {
711                tracing::debug!("Loaded agent '{}' from {}", agent.name, path.display());
712                agents.push(agent);
713            }
714            Err(e) => {
715                tracing::warn!("Failed to parse agent file {}: {}", path.display(), e);
716            }
717        }
718    }
719
720    agents
721}
722
723/// Create built-in agent definitions
724pub fn builtin_agents() -> Vec<AgentDefinition> {
725    vec![
726        // Explore agent: Fast codebase exploration (read-only)
727        AgentDefinition::new(
728            "explore",
729            "Fast codebase exploration agent. Use for searching files, reading code, \
730             and understanding codebase structure. Read-only operations only.",
731        )
732        .native()
733        .with_permissions(explore_permissions())
734        .with_max_steps(20)
735        .with_prompt(EXPLORE_PROMPT),
736        // General agent: Multi-step task execution
737        AgentDefinition::new(
738            "general",
739            "General-purpose agent for multi-step task execution. Can read, write, \
740             and execute commands.",
741        )
742        .native()
743        .with_permissions(general_permissions())
744        .with_max_steps(50),
745        // Plan agent: Read-only planning mode
746        AgentDefinition::new(
747            "plan",
748            "Planning agent for designing implementation approaches. Read-only access \
749             to explore codebase and create plans.",
750        )
751        .native()
752        .with_permissions(plan_permissions())
753        .with_max_steps(30)
754        .with_prompt(PLAN_PROMPT),
755        // Verification agent: adversarial validation and repro
756        AgentDefinition::new(
757            "verification",
758            "Verification agent for adversarial validation. Prefer real checks, \
759             reproductions, and regression testing over code reading alone.",
760        )
761        .native()
762        .with_permissions(verification_permissions())
763        .with_max_steps(30)
764        .with_prompt(VERIFICATION_PROMPT),
765        // Review agent: review-focused analysis
766        AgentDefinition::new(
767            "review",
768            "Code review agent focused on correctness, regressions, security, \
769             maintainability, and clear findings.",
770        )
771        .native()
772        .with_permissions(review_permissions())
773        .with_max_steps(25)
774        .with_prompt(REVIEW_PROMPT),
775    ]
776}
777
778// ============================================================================
779// Permission Policies for Built-in Agents
780// ============================================================================
781
782/// Permission policy for explore agent (read-only)
783fn explore_permissions() -> PermissionPolicy {
784    PermissionPolicy::new()
785        .allow_all(&["read", "grep", "glob", "ls"])
786        .deny_all(&["write", "edit", "task"])
787        .allow("Bash(ls:*)")
788        .allow("Bash(cat:*)")
789        .allow("Bash(head:*)")
790        .allow("Bash(tail:*)")
791        .allow("Bash(find:*)")
792        .allow("Bash(wc:*)")
793        .deny("Bash(rm:*)")
794        .deny("Bash(mv:*)")
795        .deny("Bash(cp:*)")
796}
797
798/// Permission policy for general agent (full access except task)
799fn general_permissions() -> PermissionPolicy {
800    PermissionPolicy::new()
801        .allow_all(&["read", "write", "edit", "grep", "glob", "ls", "bash"])
802        .deny("task")
803}
804
805/// Permission policy for plan agent (read-only)
806fn plan_permissions() -> PermissionPolicy {
807    PermissionPolicy::new()
808        .allow_all(&["read", "grep", "glob", "ls"])
809        .deny_all(&["write", "edit", "bash", "task"])
810}
811
812/// Permission policy for verification agent (read-heavy with runtime checks)
813fn verification_permissions() -> PermissionPolicy {
814    PermissionPolicy::new()
815        .allow_all(&["read", "grep", "glob", "ls", "bash"])
816        .deny_all(&["write", "edit", "task"])
817}
818
819/// Permission policy for review agent (read-heavy with optional lightweight checks)
820fn review_permissions() -> PermissionPolicy {
821    PermissionPolicy::new()
822        .allow_all(&["read", "grep", "glob", "ls", "bash"])
823        .deny_all(&["write", "edit", "task"])
824}
825
826// ============================================================================
827// System Prompts for Built-in Agents
828// ============================================================================
829
830const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
831
832const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
833
834const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
835
836const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
837
838// ============================================================================
839// Tests
840// ============================================================================
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845
846    #[test]
847    fn test_agent_definition_builder() {
848        let agent = AgentDefinition::new("test", "Test agent")
849            .native()
850            .hidden()
851            .with_max_steps(10);
852
853        assert_eq!(agent.name, "test");
854        assert_eq!(agent.description, "Test agent");
855        assert!(agent.native);
856        assert!(agent.hidden);
857        assert_eq!(agent.max_steps, Some(10));
858    }
859
860    #[test]
861    fn test_agent_registry_new() {
862        let registry = AgentRegistry::new();
863
864        // Should have built-in agents
865        assert!(registry.exists("explore"));
866        assert!(registry.exists("general"));
867        assert!(registry.exists("plan"));
868        assert!(registry.exists("verification"));
869        assert!(registry.exists("review"));
870        assert_eq!(registry.len(), 5);
871    }
872
873    #[test]
874    fn test_agent_registry_get() {
875        let registry = AgentRegistry::new();
876
877        let explore = registry.get("explore").unwrap();
878        assert_eq!(explore.name, "explore");
879        assert!(explore.native);
880        assert!(!explore.hidden);
881
882        assert!(registry.get("nonexistent").is_none());
883    }
884
885    #[test]
886    fn test_agent_registry_register_unregister() {
887        let registry = AgentRegistry::new();
888        let initial_count = registry.len();
889
890        // Register custom agent
891        let custom = AgentDefinition::new("custom", "Custom agent");
892        registry.register(custom);
893        assert_eq!(registry.len(), initial_count + 1);
894        assert!(registry.exists("custom"));
895
896        // Unregister
897        assert!(registry.unregister("custom"));
898        assert_eq!(registry.len(), initial_count);
899        assert!(!registry.exists("custom"));
900
901        // Unregister non-existent
902        assert!(!registry.unregister("nonexistent"));
903    }
904
905    #[test]
906    fn test_agent_registry_list_visible() {
907        let registry = AgentRegistry::new();
908
909        let visible = registry.list_visible();
910        let all = registry.list();
911
912        assert_eq!(visible.len(), all.len());
913        assert!(visible.iter().all(|a| !a.hidden));
914    }
915
916    #[test]
917    fn test_builtin_agents() {
918        let agents = builtin_agents();
919
920        // Check we have expected agents
921        let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
922        assert!(names.contains(&"explore"));
923        assert!(names.contains(&"general"));
924        assert!(names.contains(&"plan"));
925        assert!(names.contains(&"verification"));
926        assert!(names.contains(&"review"));
927
928        // Check explore is read-only (has deny rules for write)
929        let explore = agents.iter().find(|a| a.name == "explore").unwrap();
930        assert!(!explore.permissions.deny.is_empty());
931    }
932
933    // ========================================================================
934    // Agent File Loading Tests
935    // ========================================================================
936
937    #[test]
938    fn test_parse_agent_yaml() {
939        let yaml = r#"
940name: test-agent
941description: A test agent
942hidden: false
943max_steps: 20
944"#;
945        let agent = parse_agent_yaml(yaml).unwrap();
946        assert_eq!(agent.name, "test-agent");
947        assert_eq!(agent.description, "A test agent");
948        assert!(!agent.hidden);
949        assert_eq!(agent.max_steps, Some(20));
950    }
951
952    #[test]
953    fn test_parse_agent_yaml_with_permissions() {
954        let yaml = r#"
955name: restricted-agent
956description: Agent with permissions
957permissions:
958  allow:
959    - rule: read
960    - rule: grep
961  deny:
962    - rule: write
963"#;
964        let agent = parse_agent_yaml(yaml).unwrap();
965        assert_eq!(agent.name, "restricted-agent");
966        assert_eq!(agent.permissions.allow.len(), 2);
967        assert_eq!(agent.permissions.deny.len(), 1);
968        // Verify that deserialized rules actually match (tool_name populated)
969        assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
970        assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
971        assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
972    }
973
974    #[test]
975    fn test_parse_agent_yaml_with_plain_string_permissions() {
976        // Users naturally write plain strings in allow/deny lists
977        let yaml = r#"
978name: plain-agent
979description: Agent with plain string permissions
980permissions:
981  allow:
982    - read
983    - grep
984    - "Bash(cargo:*)"
985  deny:
986    - write
987"#;
988        let agent = parse_agent_yaml(yaml).unwrap();
989        assert_eq!(agent.name, "plain-agent");
990        assert_eq!(agent.permissions.allow.len(), 3);
991        assert_eq!(agent.permissions.deny.len(), 1);
992        // Verify rules are functional
993        assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
994        assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
995        assert!(agent.permissions.allow[2]
996            .matches("Bash", &serde_json::json!({"command": "cargo build"})));
997        assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
998    }
999
1000    #[test]
1001    fn test_parse_worker_agent_yaml_uses_cattle_defaults() {
1002        let yaml = r#"
1003name: frontend-fixer
1004description: Disposable frontend implementer
1005kind: implementer
1006max_steps: 7
1007"#;
1008        let agent = parse_agent_yaml(yaml).unwrap();
1009
1010        assert_eq!(agent.name, "frontend-fixer");
1011        assert_eq!(agent.max_steps, Some(7));
1012        assert!(agent
1013            .permissions
1014            .allow
1015            .iter()
1016            .any(|r| r.matches("write", &serde_json::json!({}))));
1017        assert!(agent
1018            .permissions
1019            .deny
1020            .iter()
1021            .any(|r| r.matches("task", &serde_json::json!({}))));
1022    }
1023
1024    #[test]
1025    fn test_parse_agent_yaml_missing_name() {
1026        let yaml = r#"
1027description: Agent without name
1028"#;
1029        let result = parse_agent_yaml(yaml);
1030        assert!(result.is_err());
1031    }
1032
1033    #[test]
1034    fn test_parse_agent_md() {
1035        let md = r#"---
1036name: md-agent
1037description: Agent from markdown
1038max_steps: 15
1039---
1040# System Prompt
1041
1042You are a helpful agent.
1043Do your best work.
1044"#;
1045        let agent = parse_agent_md(md).unwrap();
1046        assert_eq!(agent.name, "md-agent");
1047        assert_eq!(agent.description, "Agent from markdown");
1048        assert_eq!(agent.max_steps, Some(15));
1049        assert!(agent.prompt.is_some());
1050        assert!(agent.prompt.unwrap().contains("helpful agent"));
1051    }
1052
1053    #[test]
1054    fn test_parse_agent_md_with_prompt_in_frontmatter() {
1055        let md = r#"---
1056name: prompt-agent
1057description: Agent with prompt in frontmatter
1058prompt: "Frontmatter prompt"
1059---
1060Body content that should be ignored
1061"#;
1062        let agent = parse_agent_md(md).unwrap();
1063        assert_eq!(agent.prompt.unwrap(), "Frontmatter prompt");
1064    }
1065
1066    #[test]
1067    fn test_parse_worker_agent_md_uses_body_prompt() {
1068        let md = r#"---
1069name: review-cow
1070description: Disposable review worker
1071kind: reviewer
1072---
1073Review only the staged diff and return prioritized findings.
1074"#;
1075        let agent = parse_agent_md(md).unwrap();
1076
1077        assert_eq!(agent.name, "review-cow");
1078        assert_eq!(
1079            agent.prompt.as_deref(),
1080            Some("Review only the staged diff and return prioritized findings.")
1081        );
1082        assert!(agent
1083            .permissions
1084            .deny
1085            .iter()
1086            .any(|r| r.matches("write", &serde_json::json!({}))));
1087    }
1088
1089    #[test]
1090    fn test_parse_agent_md_missing_frontmatter() {
1091        let md = "Just markdown without frontmatter";
1092        let result = parse_agent_md(md);
1093        assert!(result.is_err());
1094    }
1095
1096    #[test]
1097    fn test_load_agents_from_dir() {
1098        let temp_dir = tempfile::tempdir().unwrap();
1099
1100        // Create a YAML agent file
1101        std::fs::write(
1102            temp_dir.path().join("agent1.yaml"),
1103            r#"
1104name: yaml-agent
1105description: Agent from YAML file
1106"#,
1107        )
1108        .unwrap();
1109
1110        // Create a Markdown agent file
1111        std::fs::write(
1112            temp_dir.path().join("agent2.md"),
1113            r#"---
1114name: md-agent
1115description: Agent from Markdown file
1116---
1117System prompt here
1118"#,
1119        )
1120        .unwrap();
1121
1122        // Create an invalid file (should be skipped)
1123        std::fs::write(temp_dir.path().join("invalid.yaml"), "not: valid: yaml: [").unwrap();
1124
1125        // Create a non-agent file (should be skipped)
1126        std::fs::write(temp_dir.path().join("readme.txt"), "Just a text file").unwrap();
1127
1128        let agents = load_agents_from_dir(temp_dir.path());
1129        assert_eq!(agents.len(), 2);
1130
1131        let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
1132        assert!(names.contains(&"yaml-agent"));
1133        assert!(names.contains(&"md-agent"));
1134    }
1135
1136    #[test]
1137    fn test_load_agents_from_nonexistent_dir() {
1138        let agents = load_agents_from_dir(std::path::Path::new("/nonexistent/dir"));
1139        assert!(agents.is_empty());
1140    }
1141
1142    #[test]
1143    fn test_registry_with_config() {
1144        let temp_dir = tempfile::tempdir().unwrap();
1145
1146        // Create an agent file
1147        std::fs::write(
1148            temp_dir.path().join("custom.yaml"),
1149            r#"
1150name: custom-agent
1151description: Custom agent from config
1152"#,
1153        )
1154        .unwrap();
1155
1156        let config = CodeConfig::new().add_agent_dir(temp_dir.path());
1157        let registry = AgentRegistry::with_config(&config);
1158
1159        // Should have built-in agents plus custom agent
1160        assert!(registry.exists("explore"));
1161        assert!(registry.exists("custom-agent"));
1162        assert_eq!(registry.len(), 6); // 5 built-in + 1 custom
1163    }
1164
1165    #[test]
1166    fn test_agent_definition_with_model() {
1167        let model = ModelConfig {
1168            model: "claude-3-5-sonnet".to_string(),
1169            provider: Some("anthropic".to_string()),
1170        };
1171        let agent = AgentDefinition::new("test", "Test").with_model(model);
1172        assert!(agent.model.is_some());
1173        assert_eq!(agent.model.unwrap().provider, Some("anthropic".to_string()));
1174    }
1175
1176    #[test]
1177    fn test_model_config_from_model_ref() {
1178        let model = ModelConfig::from_model_ref("openai/gpt-4o");
1179        assert_eq!(model.provider.as_deref(), Some("openai"));
1180        assert_eq!(model.model, "gpt-4o");
1181        assert_eq!(model.model_ref(), "openai/gpt-4o");
1182
1183        let inherited = ModelConfig::from_model_ref("claude-sonnet");
1184        assert_eq!(inherited.provider, None);
1185        assert_eq!(inherited.model_ref(), "claude-sonnet");
1186    }
1187
1188    #[test]
1189    fn test_worker_agent_kind_from_str_accepts_aliases() {
1190        assert_eq!(
1191            "explore".parse::<WorkerAgentKind>().unwrap(),
1192            WorkerAgentKind::ReadOnly
1193        );
1194        assert_eq!(
1195            "general".parse::<WorkerAgentKind>().unwrap(),
1196            WorkerAgentKind::Implementer
1197        );
1198        assert!("unknown".parse::<WorkerAgentKind>().is_err());
1199    }
1200
1201    #[test]
1202    fn worker_spec_implementer_creates_cattle_agent_definition() {
1203        let agent = WorkerAgentSpec::implementer("frontend-fixer", "Fix frontend issues")
1204            .with_prompt("Focus on small, verified patches.")
1205            .with_provider_model("anthropic", "claude-sonnet")
1206            .with_max_steps(12)
1207            .into_agent_definition();
1208
1209        assert_eq!(agent.name, "frontend-fixer");
1210        assert_eq!(agent.max_steps, Some(12));
1211        assert_eq!(
1212            agent.prompt.as_deref(),
1213            Some("Focus on small, verified patches.")
1214        );
1215        assert_eq!(agent.model.unwrap().provider.as_deref(), Some("anthropic"));
1216        assert!(agent
1217            .permissions
1218            .allow
1219            .iter()
1220            .any(|r| r.matches("write", &serde_json::json!({}))));
1221        assert!(agent
1222            .permissions
1223            .deny
1224            .iter()
1225            .any(|r| r.matches("task", &serde_json::json!({}))));
1226    }
1227
1228    #[test]
1229    fn worker_spec_read_only_uses_safe_defaults() {
1230        let agent = WorkerAgentSpec::read_only("scanner", "Scan repository")
1231            .hidden(true)
1232            .into_agent_definition();
1233
1234        assert!(agent.hidden);
1235        assert_eq!(agent.max_steps, Some(20));
1236        assert!(agent.prompt.is_some());
1237        assert!(agent
1238            .permissions
1239            .allow
1240            .iter()
1241            .any(|r| r.matches("read", &serde_json::json!({}))));
1242        assert!(agent
1243            .permissions
1244            .deny
1245            .iter()
1246            .any(|r| r.matches("write", &serde_json::json!({}))));
1247    }
1248
1249    #[test]
1250    fn registry_register_worker_returns_and_stores_definition() {
1251        let registry = AgentRegistry::new();
1252        let agent =
1253            registry.register_worker(WorkerAgentSpec::custom("strict-worker", "Strict worker"));
1254
1255        assert_eq!(agent.name, "strict-worker");
1256        assert!(registry.exists("strict-worker"));
1257        assert_eq!(
1258            agent
1259                .permissions
1260                .check("bash", &serde_json::json!({"command":"echo hi"})),
1261            crate::permissions::PermissionDecision::Ask
1262        );
1263    }
1264
1265    #[test]
1266    fn registry_register_workers_batches_cattle_specs() {
1267        let registry = AgentRegistry::new();
1268        let agents = registry.register_workers([
1269            WorkerAgentSpec::planner("planner-cow", "Plan work"),
1270            WorkerAgentSpec::verifier("verify-cow", "Verify work"),
1271        ]);
1272
1273        assert_eq!(agents.len(), 2);
1274        assert!(registry.exists("planner-cow"));
1275        assert!(registry.exists("verify-cow"));
1276    }
1277
1278    #[test]
1279    fn test_agent_registry_default() {
1280        let registry = AgentRegistry::default();
1281        assert!(!registry.is_empty());
1282        assert_eq!(registry.len(), 5);
1283    }
1284
1285    #[test]
1286    fn test_agent_registry_is_empty() {
1287        let registry = AgentRegistry {
1288            agents: RwLock::new(HashMap::new()),
1289        };
1290        assert!(registry.is_empty());
1291        assert_eq!(registry.len(), 0);
1292    }
1293}