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;
62use std::path::Path;
63use std::sync::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    /// How child runs resolve Ask decisions. Default: AutoApprove when
429    /// the agent has explicit allow rules, DenyOnAsk otherwise.
430    #[serde(default, skip_serializing_if = "Option::is_none")]
431    pub confirmation_inheritance: Option<ConfirmationInheritance>,
432}
433
434impl AgentDefinition {
435    /// Create a new agent definition
436    pub fn new(name: &str, description: &str) -> Self {
437        Self {
438            name: name.to_string(),
439            description: description.to_string(),
440            native: false,
441            hidden: false,
442            permissions: PermissionPolicy::default(),
443            model: None,
444            prompt: None,
445            max_steps: None,
446            confirmation_inheritance: None,
447        }
448    }
449
450    /// Create an agent definition from a disposable worker recipe.
451    pub fn worker(spec: WorkerAgentSpec) -> Self {
452        spec.into_agent_definition()
453    }
454
455    /// Mark as native (built-in)
456    pub fn native(mut self) -> Self {
457        self.native = true;
458        self
459    }
460
461    /// Mark as hidden from UI
462    pub fn hidden(mut self) -> Self {
463        self.hidden = true;
464        self
465    }
466
467    /// Set permission policy
468    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
469        self.permissions = permissions;
470        self
471    }
472
473    /// Set model override
474    pub fn with_model(mut self, model: ModelConfig) -> Self {
475        self.model = Some(model);
476        self
477    }
478
479    /// Set system prompt
480    pub fn with_prompt(mut self, prompt: &str) -> Self {
481        self.prompt = Some(prompt.to_string());
482        self
483    }
484
485    /// Set maximum execution steps
486    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
487        self.max_steps = Some(max_steps);
488        self
489    }
490
491    /// Whether this definition has non-empty permission rules.
492    pub fn has_defined_permissions(&self) -> bool {
493        !self.permissions.allow.is_empty() || !self.permissions.deny.is_empty()
494    }
495
496    /// Apply this definition's declared configuration to a mutable AgentConfig.
497    ///
498    /// Follows the "host overrides win" principle: only fills fields that are
499    /// currently at their default/None state. Callers who want to force values
500    /// should set them *after* calling `apply_to`.
501    pub(crate) fn apply_to(&self, config: &mut crate::agent::AgentConfig) {
502        use std::sync::Arc;
503
504        if config.permission_checker.is_none() && self.has_defined_permissions() {
505            config.permission_checker =
506                Some(Arc::new(self.permissions.clone()) as Arc<dyn PermissionChecker>);
507            config.permission_policy = Some(self.permissions.clone());
508        }
509
510        if let Some(ref prompt) = self.prompt {
511            if config.prompt_slots.extra.is_none() {
512                config.prompt_slots.extra = Some(prompt.clone());
513            }
514        }
515
516        if let Some(max_steps) = self.max_steps {
517            if config.max_tool_rounds == crate::agent::MAX_TOOL_ROUNDS {
518                config.max_tool_rounds = max_steps;
519            }
520        }
521
522        // Confirmation inheritance: resolve Ask decisions in child runs.
523        if config.confirmation_manager.is_none() {
524            let inheritance = self.confirmation_inheritance.clone().unwrap_or_else(|| {
525                if self.has_defined_permissions() {
526                    ConfirmationInheritance::AutoApprove
527                } else {
528                    ConfirmationInheritance::DenyOnAsk
529                }
530            });
531            match inheritance {
532                ConfirmationInheritance::AutoApprove => {
533                    config.confirmation_manager =
534                        Some(Arc::new(crate::hitl::AutoApproveConfirmation));
535                }
536                ConfirmationInheritance::DenyOnAsk => { /* leave None — safety_gate denies */ }
537                ConfirmationInheritance::InheritParent => { /* caller passes parent's manager */ }
538            }
539        }
540    }
541
542    /// Set confirmation inheritance policy for child runs.
543    pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
544        self.confirmation_inheritance = Some(inheritance);
545        self
546    }
547}
548
549/// Agent registry for managing agent definitions
550///
551/// Thread-safe registry that stores agent definitions and provides
552/// lookup functionality.
553pub struct AgentRegistry {
554    agents: RwLock<HashMap<String, AgentDefinition>>,
555}
556
557fn canonical_agent_name(name: &str) -> &str {
558    match name.trim() {
559        "general-purpose" | "general_purpose" | "generalpurpose" => "general",
560        "deep_research" | "deepresearch" => "deep-research",
561        "verify" | "verifier" => "verification",
562        "code-review" | "code_reviewer" | "reviewer" => "review",
563        other => other,
564    }
565}
566
567impl Default for AgentRegistry {
568    fn default() -> Self {
569        Self::new()
570    }
571}
572
573impl AgentRegistry {
574    /// Create a new agent registry with built-in agents
575    pub fn new() -> Self {
576        let registry = Self {
577            agents: RwLock::new(HashMap::new()),
578        };
579
580        // Register built-in agents
581        for agent in builtin_agents() {
582            registry.register(agent);
583        }
584
585        registry
586    }
587
588    /// Create a new agent registry with configuration
589    ///
590    /// Loads built-in agents first, then loads agents from configured directories.
591    pub fn with_config(config: &CodeConfig) -> Self {
592        let registry = Self::new();
593
594        // Load agents from configured directories
595        for dir in &config.agent_dirs {
596            let agents = load_agents_from_dir(dir);
597            for agent in agents {
598                tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
599                registry.register(agent);
600            }
601        }
602
603        registry
604    }
605
606    /// Register an agent definition
607    pub fn register(&self, agent: AgentDefinition) {
608        let mut agents = write_or_recover(&self.agents);
609        tracing::debug!("Registering agent: {}", agent.name);
610        agents.insert(agent.name.clone(), agent);
611    }
612
613    /// Register a disposable worker agent from a reproducible spec.
614    ///
615    /// Returns the compiled [`AgentDefinition`] so callers can inspect or pass it
616    /// directly to `session_for_agent`.
617    pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
618        let agent = spec.into_agent_definition();
619        self.register(agent.clone());
620        agent
621    }
622
623    /// Register multiple disposable worker agents and return their definitions.
624    pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
625    where
626        I: IntoIterator<Item = WorkerAgentSpec>,
627    {
628        specs
629            .into_iter()
630            .map(|spec| self.register_worker(spec))
631            .collect()
632    }
633
634    /// Unregister an agent by name
635    ///
636    /// Returns true if the agent was removed, false if not found.
637    pub fn unregister(&self, name: &str) -> bool {
638        let mut agents = write_or_recover(&self.agents);
639        agents.remove(name).is_some()
640    }
641
642    /// Get an agent definition by name
643    pub fn get(&self, name: &str) -> Option<AgentDefinition> {
644        let agents = read_or_recover(&self.agents);
645        agents
646            .get(name)
647            .or_else(|| agents.get(canonical_agent_name(name)))
648            .cloned()
649    }
650
651    /// List all registered agents
652    pub fn list(&self) -> Vec<AgentDefinition> {
653        let agents = read_or_recover(&self.agents);
654        agents.values().cloned().collect()
655    }
656
657    /// List visible agents (not hidden)
658    pub fn list_visible(&self) -> Vec<AgentDefinition> {
659        let agents = read_or_recover(&self.agents);
660        agents.values().filter(|a| !a.hidden).cloned().collect()
661    }
662
663    /// Check if an agent exists
664    pub fn exists(&self, name: &str) -> bool {
665        let agents = read_or_recover(&self.agents);
666        agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
667    }
668
669    /// Get the number of registered agents
670    pub fn len(&self) -> usize {
671        let agents = read_or_recover(&self.agents);
672        agents.len()
673    }
674
675    /// Check if the registry is empty
676    pub fn is_empty(&self) -> bool {
677        self.len() == 0
678    }
679}
680
681// ============================================================================
682// Agent File Loading
683// ============================================================================
684
685#[path = "subagent/loader.rs"]
686mod loader;
687pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
688
689/// Create built-in agent definitions
690pub fn builtin_agents() -> Vec<AgentDefinition> {
691    vec![
692        // Explore agent: Fast codebase exploration (read-only)
693        AgentDefinition::new(
694            "explore",
695            "Fast read-only exploration agent. Use for searching files, reading code, \
696             understanding codebase structure, and gathering external web evidence.",
697        )
698        .native()
699        .with_permissions(explore_permissions())
700        .with_max_steps(20)
701        .with_prompt(EXPLORE_PROMPT),
702        // General agent: Multi-step task execution
703        AgentDefinition::new(
704            "general",
705            "General-purpose agent for multi-step task execution. Can read, write, \
706             and execute commands.",
707        )
708        .native()
709        .with_permissions(general_permissions())
710        .with_max_steps(50),
711        // DeepResearch evidence agent: inherit the parent session's tool/skill
712        // policy instead of applying explore's read-only default-deny policy.
713        AgentDefinition::new(
714            "deep-research",
715            "DeepResearch evidence agent. Inherits the parent session's tools, \
716             skills, permissions, and HITL policy for bounded evidence collection.",
717        )
718        .native()
719        .hidden()
720        .with_confirmation(ConfirmationInheritance::InheritParent)
721        .with_max_steps(50),
722        // Plan agent: Read-only planning mode
723        AgentDefinition::new(
724            "plan",
725            "Planning agent for designing implementation approaches. Read-only access \
726             to explore codebase and create plans.",
727        )
728        .native()
729        .with_permissions(plan_permissions())
730        .with_max_steps(30)
731        .with_prompt(PLAN_PROMPT),
732        // Verification agent: adversarial validation and repro
733        AgentDefinition::new(
734            "verification",
735            "Verification agent for adversarial validation. Prefer real checks, \
736             reproductions, and regression testing over code reading alone.",
737        )
738        .native()
739        .with_permissions(verification_permissions())
740        .with_max_steps(30)
741        .with_prompt(VERIFICATION_PROMPT),
742        // Review agent: review-focused analysis
743        AgentDefinition::new(
744            "review",
745            "Code review agent focused on correctness, regressions, security, \
746             maintainability, and clear findings.",
747        )
748        .native()
749        .with_permissions(review_permissions())
750        .with_max_steps(25)
751        .with_prompt(REVIEW_PROMPT),
752    ]
753}
754
755// ============================================================================
756// Permission Policies for Built-in Agents
757// ============================================================================
758
759/// Permission policy for explore agent (read-only)
760fn explore_permissions() -> PermissionPolicy {
761    let mut policy = PermissionPolicy::new()
762        .allow_all(&["read", "grep", "glob", "ls", "web_fetch", "web_search"])
763        .deny_all(&["write", "edit", "task", "parallel_task"])
764        .allow("Bash(ls:*)")
765        .allow("Bash(cat:*)")
766        .allow("Bash(head:*)")
767        .allow("Bash(tail:*)")
768        .allow("Bash(find:*)")
769        .allow("Bash(wc:*)")
770        .deny("Bash(rm:*)")
771        .deny("Bash(mv:*)")
772        .deny("Bash(cp:*)");
773    policy.default_decision = PermissionDecision::Deny;
774    policy
775}
776
777/// Permission policy for general agent (full access except task)
778fn general_permissions() -> PermissionPolicy {
779    PermissionPolicy::new()
780        .allow_all(&[
781            "read",
782            "write",
783            "edit",
784            "grep",
785            "glob",
786            "ls",
787            "bash",
788            "web_fetch",
789            "web_search",
790            "git",
791            "patch",
792            "batch",
793            "generate_object",
794        ])
795        .deny("task")
796        .deny("parallel_task")
797}
798
799/// Permission policy for plan agent (read-only)
800fn plan_permissions() -> PermissionPolicy {
801    let mut policy = PermissionPolicy::new()
802        .allow_all(&["read", "grep", "glob", "ls"])
803        .deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
804    policy.default_decision = PermissionDecision::Deny;
805    policy
806}
807
808/// Permission policy for verification agent (read-heavy with runtime checks)
809fn verification_permissions() -> PermissionPolicy {
810    let mut policy = PermissionPolicy::new()
811        .allow_all(&[
812            "read",
813            "grep",
814            "glob",
815            "ls",
816            "bash",
817            "web_fetch",
818            "web_search",
819        ])
820        .deny_all(&["write", "edit", "task", "parallel_task"]);
821    policy.default_decision = PermissionDecision::Deny;
822    policy
823}
824
825/// Permission policy for review agent (read-heavy with optional lightweight checks)
826fn review_permissions() -> PermissionPolicy {
827    let mut policy = PermissionPolicy::new()
828        .allow_all(&[
829            "read",
830            "grep",
831            "glob",
832            "ls",
833            "bash",
834            "web_fetch",
835            "web_search",
836        ])
837        .deny_all(&["write", "edit", "task", "parallel_task"]);
838    policy.default_decision = PermissionDecision::Deny;
839    policy
840}
841
842// ============================================================================
843// System Prompts for Built-in Agents
844// ============================================================================
845
846const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
847
848const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
849
850const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
851
852const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
853
854// ============================================================================
855// Tests
856// ============================================================================
857
858#[cfg(test)]
859#[path = "subagent/tests.rs"]
860mod tests;