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    /// 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: resolve Ask decisions in child runs.
542        if config.confirmation_manager.is_none() {
543            let inheritance = self.confirmation_inheritance.clone().unwrap_or_else(|| {
544                if self.has_defined_permissions() {
545                    ConfirmationInheritance::AutoApprove
546                } else {
547                    ConfirmationInheritance::DenyOnAsk
548                }
549            });
550            match inheritance {
551                ConfirmationInheritance::AutoApprove => {
552                    config.confirmation_manager =
553                        Some(Arc::new(crate::hitl::AutoApproveConfirmation));
554                }
555                ConfirmationInheritance::DenyOnAsk => { /* leave None — safety_gate denies */ }
556                ConfirmationInheritance::InheritParent => { /* caller passes parent's manager */ }
557            }
558        }
559    }
560
561    /// Set confirmation inheritance policy for child runs.
562    pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
563        self.confirmation_inheritance = Some(inheritance);
564        self
565    }
566}
567
568/// Agent registry for managing agent definitions
569///
570/// Thread-safe registry that stores agent definitions and provides
571/// lookup functionality.
572pub struct AgentRegistry {
573    agents: RwLock<HashMap<String, AgentDefinition>>,
574}
575
576fn canonical_agent_name(name: &str) -> &str {
577    match name.trim() {
578        "general-purpose" | "general_purpose" | "generalpurpose" => "general",
579        "deep_research" | "deepresearch" => "deep-research",
580        "verify" | "verifier" => "verification",
581        "code-review" | "code_reviewer" | "reviewer" => "review",
582        other => other,
583    }
584}
585
586impl Default for AgentRegistry {
587    fn default() -> Self {
588        Self::new()
589    }
590}
591
592impl AgentRegistry {
593    /// Create a new agent registry with built-in agents
594    pub fn new() -> Self {
595        let registry = Self {
596            agents: RwLock::new(HashMap::new()),
597        };
598
599        // Register built-in agents
600        for agent in builtin_agents() {
601            registry.register(agent);
602        }
603
604        registry
605    }
606
607    /// Create a new agent registry with configuration
608    ///
609    /// Loads built-in agents first, then loads agents from configured directories.
610    pub fn with_config(config: &CodeConfig) -> Self {
611        let registry = Self::new();
612
613        // Load agents from configured directories
614        for dir in &config.agent_dirs {
615            let agents = load_agents_from_dir(dir);
616            for agent in agents {
617                tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
618                registry.register(agent);
619            }
620        }
621
622        registry
623    }
624
625    /// Register an agent definition
626    pub fn register(&self, agent: AgentDefinition) {
627        let mut agents = write_or_recover(&self.agents);
628        tracing::debug!("Registering agent: {}", agent.name);
629        agents.insert(agent.name.clone(), agent);
630    }
631
632    /// Register a disposable worker agent from a reproducible spec.
633    ///
634    /// Returns the compiled [`AgentDefinition`] so callers can inspect or pass it
635    /// directly to `session_for_agent`.
636    pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
637        let agent = spec.into_agent_definition();
638        self.register(agent.clone());
639        agent
640    }
641
642    /// Register multiple disposable worker agents and return their definitions.
643    pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
644    where
645        I: IntoIterator<Item = WorkerAgentSpec>,
646    {
647        specs
648            .into_iter()
649            .map(|spec| self.register_worker(spec))
650            .collect()
651    }
652
653    /// Unregister an agent by name
654    ///
655    /// Returns true if the agent was removed, false if not found.
656    pub fn unregister(&self, name: &str) -> bool {
657        let mut agents = write_or_recover(&self.agents);
658        agents.remove(name).is_some()
659    }
660
661    /// Get an agent definition by name
662    pub fn get(&self, name: &str) -> Option<AgentDefinition> {
663        let agents = read_or_recover(&self.agents);
664        agents
665            .get(name)
666            .or_else(|| agents.get(canonical_agent_name(name)))
667            .cloned()
668    }
669
670    /// List all registered agents
671    pub fn list(&self) -> Vec<AgentDefinition> {
672        let agents = read_or_recover(&self.agents);
673        agents.values().cloned().collect()
674    }
675
676    /// List visible agents (not hidden)
677    pub fn list_visible(&self) -> Vec<AgentDefinition> {
678        let agents = read_or_recover(&self.agents);
679        agents.values().filter(|a| !a.hidden).cloned().collect()
680    }
681
682    /// Check if an agent exists
683    pub fn exists(&self, name: &str) -> bool {
684        let agents = read_or_recover(&self.agents);
685        agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
686    }
687
688    /// Get the number of registered agents
689    pub fn len(&self) -> usize {
690        let agents = read_or_recover(&self.agents);
691        agents.len()
692    }
693
694    /// Check if the registry is empty
695    pub fn is_empty(&self) -> bool {
696        self.len() == 0
697    }
698}
699
700// ============================================================================
701// Agent File Loading
702// ============================================================================
703
704#[path = "subagent/loader.rs"]
705mod loader;
706pub use loader::{load_agents_from_dir, parse_agent_md, parse_agent_yaml};
707
708/// Create built-in agent definitions
709pub fn builtin_agents() -> Vec<AgentDefinition> {
710    vec![
711        // Explore agent: Fast codebase exploration (read-only)
712        AgentDefinition::new(
713            "explore",
714            "Fast read-only exploration agent. Use for searching files, reading code, \
715             understanding codebase structure, and gathering external web evidence.",
716        )
717        .native()
718        .with_permissions(explore_permissions())
719        .with_max_steps(20)
720        .with_prompt(EXPLORE_PROMPT),
721        // General agent: Multi-step task execution
722        AgentDefinition::new(
723            "general",
724            "General-purpose agent for multi-step task execution. Can read, write, \
725             and execute commands.",
726        )
727        .native()
728        .with_permissions(general_permissions())
729        .with_max_steps(50),
730        // DeepResearch evidence agent: inherit the parent session's tool/skill
731        // policy instead of applying explore's read-only default-deny policy.
732        AgentDefinition::new(
733            "deep-research",
734            "DeepResearch evidence agent. Inherits the parent session's tools, \
735             skills, permissions, and HITL policy for bounded evidence collection.",
736        )
737        .native()
738        .hidden()
739        .with_confirmation(ConfirmationInheritance::InheritParent)
740        .with_max_steps(50),
741        // Loop Engineering decision roles are intentionally tool-free. Makers
742        // gather evidence; planners and checkers only make structured decisions.
743        AgentDefinition::new(
744            "loop-planner",
745            "Tool-free semantic planner for engineered loops.",
746        )
747        .native()
748        .hidden()
749        .tool_free()
750        .with_max_steps(4)
751        .with_prompt(LOOP_PLANNER_PROMPT),
752        AgentDefinition::new(
753            "loop-checker",
754            "Tool-free independent checker for engineered loops.",
755        )
756        .native()
757        .hidden()
758        .tool_free()
759        .with_max_steps(4)
760        .with_prompt(LOOP_CHECKER_PROMPT),
761        // Plan agent: Read-only planning mode
762        AgentDefinition::new(
763            "plan",
764            "Planning agent for designing implementation approaches. Read-only access \
765             to explore codebase and create plans.",
766        )
767        .native()
768        .with_permissions(plan_permissions())
769        .with_max_steps(30)
770        .with_prompt(PLAN_PROMPT),
771        // Verification agent: adversarial validation and repro
772        AgentDefinition::new(
773            "verification",
774            "Verification agent for adversarial validation. Prefer real checks, \
775             reproductions, and regression testing over code reading alone.",
776        )
777        .native()
778        .with_permissions(verification_permissions())
779        .with_max_steps(30)
780        .with_prompt(VERIFICATION_PROMPT),
781        // Review agent: review-focused analysis
782        AgentDefinition::new(
783            "review",
784            "Code review agent focused on correctness, regressions, security, \
785             maintainability, and clear findings.",
786        )
787        .native()
788        .with_permissions(review_permissions())
789        .with_max_steps(25)
790        .with_prompt(REVIEW_PROMPT),
791    ]
792}
793
794// ============================================================================
795// Permission Policies for Built-in Agents
796// ============================================================================
797
798/// Permission policy for explore agent (read-only)
799fn explore_permissions() -> PermissionPolicy {
800    let mut policy = PermissionPolicy::new()
801        .allow_all(&["read", "grep", "glob", "ls", "web_fetch", "web_search"])
802        .deny_all(&["write", "edit", "task", "parallel_task"])
803        .allow("Bash(ls:*)")
804        .allow("Bash(cat:*)")
805        .allow("Bash(head:*)")
806        .allow("Bash(tail:*)")
807        .allow("Bash(find:*)")
808        .allow("Bash(wc:*)")
809        .deny("Bash(rm:*)")
810        .deny("Bash(mv:*)")
811        .deny("Bash(cp:*)");
812    policy.default_decision = PermissionDecision::Deny;
813    policy
814}
815
816/// Permission policy for general agent (full access except task)
817fn general_permissions() -> PermissionPolicy {
818    PermissionPolicy::new()
819        .allow_all(&[
820            "read",
821            "write",
822            "edit",
823            "grep",
824            "glob",
825            "ls",
826            "bash",
827            "web_fetch",
828            "web_search",
829            "git",
830            "patch",
831            "batch",
832            "generate_object",
833        ])
834        .deny("task")
835        .deny("parallel_task")
836}
837
838/// Permission policy for plan agent (read-only)
839fn plan_permissions() -> PermissionPolicy {
840    let mut policy = PermissionPolicy::new()
841        .allow_all(&["read", "grep", "glob", "ls"])
842        .deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
843    policy.default_decision = PermissionDecision::Deny;
844    policy
845}
846
847/// Permission policy for verification agent (read-heavy with runtime checks)
848fn verification_permissions() -> PermissionPolicy {
849    let mut policy = PermissionPolicy::new()
850        .allow_all(&[
851            "read",
852            "grep",
853            "glob",
854            "ls",
855            "bash",
856            "web_fetch",
857            "web_search",
858        ])
859        .deny_all(&["write", "edit", "task", "parallel_task"]);
860    policy.default_decision = PermissionDecision::Deny;
861    policy
862}
863
864/// Permission policy for review agent (read-heavy with optional lightweight checks)
865fn review_permissions() -> PermissionPolicy {
866    let mut policy = PermissionPolicy::new()
867        .allow_all(&[
868            "read",
869            "grep",
870            "glob",
871            "ls",
872            "bash",
873            "web_fetch",
874            "web_search",
875        ])
876        .deny_all(&["write", "edit", "task", "parallel_task"]);
877    policy.default_decision = PermissionDecision::Deny;
878    policy
879}
880
881// ============================================================================
882// System Prompts for Built-in Agents
883// ============================================================================
884
885const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
886
887const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
888
889const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
890
891const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
892
893const 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.";
894
895const 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.";
896
897// ============================================================================
898// Tests
899// ============================================================================
900
901#[cfg(test)]
902#[path = "subagent/tests.rs"]
903mod tests;