Skip to main content

agent_sdk/agent/
subagent.rs

1//! Subagent — a lightweight, isolated agent that runs a focused task and returns
2//! results to the caller.
3//!
4//! Unlike agent teams where teammates communicate with each other via mailboxes,
5//! subagents only report results back to the parent agent. They run in their own
6//! context window with a custom system prompt and optional tool restrictions.
7//!
8//! Subagents **cannot** spawn other subagents (no nesting).
9
10use std::path::PathBuf;
11use std::sync::Arc;
12
13use serde::{Deserialize, Serialize};
14use tokio::sync::mpsc::UnboundedSender;
15use tracing::info;
16use uuid::Uuid;
17
18use crate::error::{AgentId, SdkResult};
19use crate::tools::command_tools::RunCommandTool;
20use crate::tools::fs_tools::{ListDirectoryTool, ReadFileTool, WriteFileTool};
21use crate::tools::registry::ToolRegistry;
22use crate::tools::search_tools::SearchFilesTool;
23use crate::tools::web_tools::WebSearchTool;
24use crate::traits::llm_client::LlmClient;
25use crate::traits::tool::Tool;
26
27use super::agent_loop::{AgentLoop, AgentLoopResult};
28use super::events::AgentEvent;
29
30/// Definition of a subagent — its identity, capabilities, and constraints.
31///
32/// Analogous to a markdown frontmatter file in Claude Code's subagent system.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SubAgentDef {
35    /// Unique identifier (lowercase, hyphens). E.g. "code-reviewer".
36    pub name: String,
37    /// When the parent agent should delegate to this subagent.
38    pub description: String,
39    /// The system prompt (markdown body). Replaces the default system prompt entirely.
40    pub prompt: String,
41    /// Allowed tool names. If empty, inherits all default tools.
42    /// Use this as an allowlist: only these tools will be available.
43    #[serde(default)]
44    pub allowed_tools: Vec<String>,
45    /// Tool names to explicitly deny. Applied after `allowed_tools`.
46    #[serde(default)]
47    pub disallowed_tools: Vec<String>,
48    /// Model override. If `None`, inherits from the parent.
49    #[serde(default)]
50    pub model: Option<String>,
51    /// Maximum agentic turns before the subagent stops.
52    #[serde(default = "default_max_turns")]
53    pub max_turns: usize,
54    /// Maximum context window tokens.
55    #[serde(default = "default_max_context_tokens")]
56    pub max_context_tokens: usize,
57    /// Whether to always run this subagent in the background.
58    #[serde(default)]
59    pub background: bool,
60}
61
62fn default_max_turns() -> usize {
63    30
64}
65
66fn default_max_context_tokens() -> usize {
67    200_000
68}
69
70impl SubAgentDef {
71    /// Create a new subagent definition with required fields.
72    pub fn new(
73        name: impl Into<String>,
74        description: impl Into<String>,
75        prompt: impl Into<String>,
76    ) -> Self {
77        Self {
78            name: name.into(),
79            description: description.into(),
80            prompt: prompt.into(),
81            allowed_tools: Vec::new(),
82            disallowed_tools: Vec::new(),
83            model: None,
84            max_turns: default_max_turns(),
85            max_context_tokens: default_max_context_tokens(),
86            background: false,
87        }
88    }
89
90    /// Restrict the subagent to only these tools.
91    pub fn with_allowed_tools(mut self, tools: Vec<String>) -> Self {
92        self.allowed_tools = tools;
93        self
94    }
95
96    /// Deny specific tools (removed from inherited or allowed set).
97    pub fn with_disallowed_tools(mut self, tools: Vec<String>) -> Self {
98        self.disallowed_tools = tools;
99        self
100    }
101
102    /// Override the model for this subagent.
103    pub fn with_model(mut self, model: impl Into<String>) -> Self {
104        self.model = Some(model.into());
105        self
106    }
107
108    /// Set maximum agentic turns.
109    pub fn with_max_turns(mut self, max_turns: usize) -> Self {
110        self.max_turns = max_turns;
111        self
112    }
113
114    /// Set maximum context window tokens.
115    pub fn with_max_context_tokens(mut self, tokens: usize) -> Self {
116        self.max_context_tokens = tokens;
117        self
118    }
119
120    /// Set whether to always run in background.
121    pub fn with_background(mut self, background: bool) -> Self {
122        self.background = background;
123        self
124    }
125}
126
127/// Result returned when a subagent completes.
128#[derive(Debug, Clone, Serialize)]
129pub struct SubAgentResult {
130    pub agent_id: AgentId,
131    pub name: String,
132    pub final_content: String,
133    pub total_tokens: u64,
134    pub iterations: usize,
135    pub tool_calls_count: usize,
136}
137
138impl From<(AgentId, &str, AgentLoopResult)> for SubAgentResult {
139    fn from((agent_id, name, result): (AgentId, &str, AgentLoopResult)) -> Self {
140        Self {
141            agent_id,
142            name: name.to_string(),
143            final_content: result.final_content,
144            total_tokens: result.total_tokens,
145            iterations: result.iterations,
146            tool_calls_count: result.tool_calls_count,
147        }
148    }
149}
150
151/// Runs a subagent to completion.
152///
153/// The subagent gets its own `AgentLoop` with its own context window. It runs
154/// the given task prompt and returns the result. Subagents cannot spawn other
155/// subagents — the `spawn_subagent` tool is intentionally excluded.
156pub struct SubAgentRunner {
157    pub work_dir: PathBuf,
158    pub source_root: PathBuf,
159    pub llm_client: Arc<dyn LlmClient>,
160    pub event_tx: Option<UnboundedSender<AgentEvent>>,
161    /// Optional override LLM client (for model override).
162    pub override_llm_client: Option<Arc<dyn LlmClient>>,
163}
164
165impl SubAgentRunner {
166    pub fn new(
167        work_dir: PathBuf,
168        source_root: PathBuf,
169        llm_client: Arc<dyn LlmClient>,
170    ) -> Self {
171        Self {
172            work_dir,
173            source_root,
174            llm_client,
175            event_tx: None,
176            override_llm_client: None,
177        }
178    }
179
180    pub fn with_event_sink(mut self, tx: UnboundedSender<AgentEvent>) -> Self {
181        self.event_tx = Some(tx);
182        self
183    }
184
185    pub fn with_override_llm_client(mut self, client: Arc<dyn LlmClient>) -> Self {
186        self.override_llm_client = Some(client);
187        self
188    }
189
190    /// Run a subagent with the given definition and task prompt.
191    ///
192    /// Returns the subagent's result including final content and token usage.
193    pub async fn run(
194        &self,
195        def: &SubAgentDef,
196        task_prompt: &str,
197    ) -> SdkResult<SubAgentResult> {
198        let agent_id = Uuid::new_v4();
199        let client = self
200            .override_llm_client
201            .clone()
202            .unwrap_or_else(|| self.llm_client.clone());
203
204        info!(
205            agent_id = %agent_id,
206            subagent = %def.name,
207            "Spawning subagent"
208        );
209
210        self.emit(AgentEvent::SubAgentSpawned {
211            agent_id,
212            name: def.name.clone(),
213            description: def.description.clone(),
214        });
215
216        // Build tool registry with restrictions
217        let tools = self.build_tools(def);
218
219        // Build system prompt
220        let system_prompt = crate::prompts::subagent_system_prompt(
221            &def.prompt,
222            &self.source_root,
223            &self.work_dir,
224        );
225
226        let mut agent_loop = AgentLoop::new(
227            agent_id,
228            client,
229            tools,
230            system_prompt,
231            def.max_turns,
232        )
233        .with_max_context_tokens(def.max_context_tokens)
234        .with_agent_name(&def.name);
235
236        if let Some(ref tx) = self.event_tx {
237            agent_loop.set_event_sink(tx.clone());
238        }
239
240        match agent_loop.run(task_prompt.to_string()).await {
241            Ok(loop_result) => {
242                let result = SubAgentResult::from((agent_id, def.name.as_str(), loop_result));
243
244                self.emit(AgentEvent::SubAgentCompleted {
245                    agent_id,
246                    name: def.name.clone(),
247                    tokens_used: result.total_tokens,
248                    iterations: result.iterations,
249                    tool_calls: result.tool_calls_count,
250                    final_content: result.final_content.clone(),
251                });
252
253                Ok(result)
254            }
255            Err(e) => {
256                self.emit(AgentEvent::SubAgentFailed {
257                    agent_id,
258                    name: def.name.clone(),
259                    error: e.to_string(),
260                });
261                Err(e)
262            }
263        }
264    }
265
266    /// Run a subagent in the background, returning a handle to await the result.
267    pub fn run_background(
268        &self,
269        def: SubAgentDef,
270        task_prompt: String,
271    ) -> tokio::task::JoinHandle<SdkResult<SubAgentResult>> {
272        let runner = SubAgentRunner {
273            work_dir: self.work_dir.clone(),
274            source_root: self.source_root.clone(),
275            llm_client: self.llm_client.clone(),
276            event_tx: self.event_tx.clone(),
277            override_llm_client: self.override_llm_client.clone(),
278        };
279
280        tokio::spawn(async move { runner.run(&def, &task_prompt).await })
281    }
282
283    /// Build the tool registry for a subagent, respecting allowed/disallowed lists.
284    fn build_tools(&self, def: &SubAgentDef) -> ToolRegistry {
285        // NOTE: spawn_subagent and spawn_agent_team are intentionally NOT included.
286        // Subagents cannot spawn other subagents (no nesting).
287        let all_tools: Vec<(String, Arc<dyn Tool>)> = vec![
288            (
289                "read_file".to_string(),
290                Arc::new(ReadFileTool {
291                    source_root: self.source_root.clone(),
292                    work_dir: self.work_dir.clone(),
293                }),
294            ),
295            (
296                "write_file".to_string(),
297                Arc::new(WriteFileTool {
298                    work_dir: self.work_dir.clone(),
299                }),
300            ),
301            (
302                "list_directory".to_string(),
303                Arc::new(ListDirectoryTool {
304                    source_root: self.source_root.clone(),
305                    work_dir: self.work_dir.clone(),
306                }),
307            ),
308            (
309                "search_files".to_string(),
310                Arc::new(SearchFilesTool {
311                    source_root: self.source_root.clone(),
312                }),
313            ),
314            (
315                "web_search".to_string(),
316                Arc::new(WebSearchTool),
317            ),
318            (
319                "run_command".to_string(),
320                Arc::new(RunCommandTool::with_defaults(self.work_dir.clone())),
321            ),
322        ];
323
324        let allowed_set: Option<std::collections::HashSet<&str>> =
325            if def.allowed_tools.is_empty() {
326                None
327            } else {
328                Some(def.allowed_tools.iter().map(|s| s.as_str()).collect())
329            };
330        let denied_set: std::collections::HashSet<&str> =
331            def.disallowed_tools.iter().map(|s| s.as_str()).collect();
332
333        let mut registry = ToolRegistry::new();
334        for (name, tool) in all_tools {
335            let pass_allow = match &allowed_set {
336                Some(set) => set.contains(name.as_str()),
337                None => true,
338            };
339            let pass_deny = !denied_set.contains(name.as_str());
340            if pass_allow && pass_deny {
341                registry.register(tool);
342            }
343        }
344
345        registry
346    }
347
348    fn emit(&self, event: AgentEvent) {
349        if let Some(ref tx) = self.event_tx {
350            let _ = tx.send(event);
351        }
352    }
353}
354
355/// A registry of subagent definitions available for the agent to invoke.
356#[derive(Debug, Clone, Default)]
357pub struct SubAgentRegistry {
358    defs: Vec<SubAgentDef>,
359}
360
361impl SubAgentRegistry {
362    pub fn new() -> Self {
363        Self { defs: Vec::new() }
364    }
365
366    /// Register a subagent definition.
367    pub fn register(&mut self, def: SubAgentDef) {
368        // Replace existing definition with same name
369        self.defs.retain(|d| d.name != def.name);
370        self.defs.push(def);
371    }
372
373    /// Get a subagent definition by name.
374    pub fn get(&self, name: &str) -> Option<&SubAgentDef> {
375        self.defs.iter().find(|d| d.name == name)
376    }
377
378    /// List all registered subagent definitions.
379    pub fn list(&self) -> &[SubAgentDef] {
380        &self.defs
381    }
382
383    /// Check if any subagents are registered.
384    pub fn is_empty(&self) -> bool {
385        self.defs.is_empty()
386    }
387}
388
389/// Built-in subagent definitions that mirror Claude Code's defaults.
390pub fn builtin_subagents() -> Vec<SubAgentDef> {
391    vec![
392        SubAgentDef {
393            name: "explore".to_string(),
394            description: "Fast, read-only agent for searching and analyzing codebases. \
395                Use when you need to quickly find files, search code, or understand \
396                the codebase without making changes. Keeps exploration out of your \
397                main context."
398                .to_string(),
399            prompt: "You are a codebase exploration specialist. Your job is to search, \
400                read, and analyze code efficiently. Report your findings concisely.\n\n\
401                You have read-only access. Do NOT attempt to modify any files."
402                .to_string(),
403            allowed_tools: vec![
404                "read_file".to_string(),
405                "list_directory".to_string(),
406                "search_files".to_string(),
407                "run_command".to_string(),
408            ],
409            disallowed_tools: vec![
410                "write_file".to_string(),
411            ],
412            model: None,
413            max_turns: 20,
414            max_context_tokens: 200_000,
415            background: false,
416        },
417        SubAgentDef {
418            name: "plan".to_string(),
419            description: "Research agent for gathering context before presenting a plan. \
420                Use when you need to understand the codebase to plan an implementation \
421                strategy."
422                .to_string(),
423            prompt: "You are a software architect. Analyze the codebase and produce a \
424                detailed implementation plan. Include:\n\
425                1. What files need to be read/created/modified\n\
426                2. The approach and key decisions\n\
427                3. Potential risks or edge cases\n\
428                4. Verification steps\n\n\
429                You have read-only access. Do NOT attempt to modify any files."
430                .to_string(),
431            allowed_tools: vec![
432                "read_file".to_string(),
433                "list_directory".to_string(),
434                "search_files".to_string(),
435                "run_command".to_string(),
436            ],
437            disallowed_tools: vec![
438                "write_file".to_string(),
439            ],
440            model: None,
441            max_turns: 25,
442            max_context_tokens: 200_000,
443            background: false,
444        },
445        SubAgentDef {
446            name: "general-purpose".to_string(),
447            description: "Capable agent for complex, multi-step tasks requiring both \
448                exploration and action. Use for research, multi-step operations, or \
449                code modifications that benefit from isolated context."
450                .to_string(),
451            prompt: "You are an expert coding assistant handling a delegated task. \
452                Work independently and return a clear, concise result summary. \
453                Read files before modifying them. Verify your work."
454                .to_string(),
455            allowed_tools: Vec::new(), // all tools
456            disallowed_tools: Vec::new(),
457            model: None,
458            max_turns: 30,
459            max_context_tokens: 200_000,
460            background: false,
461        },
462    ]
463}