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