Skip to main content

agent_sdk/tools/
subagent_tools.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use serde::Deserialize;
6use serde_json::json;
7use uuid::Uuid;
8
9use crate::agent::agent_loop::{BackgroundResult, BackgroundResultKind};
10use crate::agent::events::AgentEvent;
11use crate::agent::subagent::{SubAgentDef, SubAgentRegistry, SubAgentRunner};
12use crate::error::{SdkError, SdkResult};
13use crate::traits::llm_client::LlmClient;
14use crate::traits::tool::{Tool, ToolDefinition};
15
16/// Tool that lets the main agent spawn a subagent for a focused task.
17///
18/// The subagent runs in its own context window, does its work, and returns
19/// results to the caller. Subagents cannot spawn other subagents.
20///
21/// The agent can either reference a registered subagent by name, or provide
22/// an inline definition with a custom prompt and tool restrictions.
23pub struct SpawnSubAgentTool {
24    pub work_dir: PathBuf,
25    pub source_root: PathBuf,
26    pub llm_client: Arc<dyn LlmClient>,
27    pub event_tx: Option<tokio::sync::mpsc::UnboundedSender<AgentEvent>>,
28    pub registry: Arc<SubAgentRegistry>,
29    /// When set, background subagent results are sent back through this channel
30    /// so the parent agent loop can inject them into its conversation.
31    pub background_tx: Option<tokio::sync::mpsc::UnboundedSender<BackgroundResult>>,
32}
33
34#[derive(Debug, Deserialize)]
35struct SubAgentRequest {
36    /// Name of a registered subagent to use, OR a custom name for inline definition.
37    name: String,
38    /// The task/prompt to send to the subagent.
39    prompt: String,
40    /// Custom system prompt (for inline definition). If omitted and name matches
41    /// a registered subagent, uses the registered definition.
42    #[serde(default)]
43    system_prompt: Option<String>,
44    /// Optional description for inline definitions.
45    #[serde(default)]
46    description: Option<String>,
47    /// Tool allowlist for inline definitions.
48    #[serde(default)]
49    allowed_tools: Vec<String>,
50    /// Tool denylist for inline definitions.
51    #[serde(default)]
52    disallowed_tools: Vec<String>,
53    /// Max agentic turns override.
54    #[serde(default)]
55    max_turns: Option<usize>,
56    /// Run in background (concurrent). Default: false (foreground/blocking).
57    #[serde(default)]
58    background: bool,
59}
60
61#[async_trait]
62impl Tool for SpawnSubAgentTool {
63    fn definition(&self) -> ToolDefinition {
64        // Build the enum of available subagent names for the LLM
65        let available: Vec<String> = self
66            .registry
67            .list()
68            .iter()
69            .map(|d| format!("{}: {}", d.name, d.description))
70            .collect();
71
72        let available_desc = if available.is_empty() {
73            "No pre-registered subagents. Provide a system_prompt for inline definition.".to_string()
74        } else {
75            format!("Available subagents:\n{}", available.join("\n"))
76        };
77
78        ToolDefinition {
79            name: "spawn_subagent".to_string(),
80            description: format!(
81                "Spawn a subagent to handle a focused task in its own context window. \
82                The subagent works independently and returns results back to you. \
83                Use this to preserve your main context by delegating exploration, \
84                research, or self-contained tasks to a subagent.\n\n\
85                You can reference a registered subagent by name, or create an inline \
86                subagent by providing a system_prompt.\n\n\
87                Subagents CANNOT spawn other subagents.\n\n\
88                {available_desc}"
89            ),
90            parameters: json!({
91                "type": "object",
92                "properties": {
93                    "name": {
94                        "type": "string",
95                        "description": "Name of the subagent. Use a registered name (e.g. 'explore', 'plan', 'general-purpose') or a custom name with system_prompt for inline definition."
96                    },
97                    "prompt": {
98                        "type": "string",
99                        "description": "The task prompt to send to the subagent. Be specific about what you need."
100                    },
101                    "system_prompt": {
102                        "type": "string",
103                        "description": "Custom system prompt for an inline subagent definition. If omitted, uses the registered definition for the given name."
104                    },
105                    "description": {
106                        "type": "string",
107                        "description": "Optional description for inline definitions."
108                    },
109                    "allowed_tools": {
110                        "type": "array",
111                        "items": { "type": "string" },
112                        "description": "Tool allowlist for inline definitions. Available: read_file, write_file, list_directory, search_files, web_search, run_command"
113                    },
114                    "disallowed_tools": {
115                        "type": "array",
116                        "items": { "type": "string" },
117                        "description": "Tool denylist. Tools listed here are removed from the available set."
118                    },
119                    "max_turns": {
120                        "type": "integer",
121                        "description": "Maximum agentic turns before the subagent stops (default: 30)."
122                    },
123                    "background": {
124                        "type": "boolean",
125                        "description": "If true, run the subagent in the background (concurrent). Default: false (blocking)."
126                    }
127                },
128                "required": ["name", "prompt"]
129            }),
130        }
131    }
132
133    async fn execute(&self, arguments: serde_json::Value) -> SdkResult<serde_json::Value> {
134        let request: SubAgentRequest =
135            serde_json::from_value(arguments).map_err(|e| SdkError::ToolExecution {
136                tool_name: "spawn_subagent".to_string(),
137                message: format!("Invalid arguments: {}", e),
138            })?;
139
140        if request.prompt.trim().is_empty() {
141            return Ok(json!({ "error": "prompt cannot be empty" }));
142        }
143
144        // Resolve the subagent definition
145        let def = if let Some(ref system_prompt) = request.system_prompt {
146            // Inline definition
147            let mut def = SubAgentDef::new(
148                &request.name,
149                request.description.as_deref().unwrap_or("Inline subagent"),
150                system_prompt,
151            );
152            if !request.allowed_tools.is_empty() {
153                def = def.with_allowed_tools(request.allowed_tools.clone());
154            }
155            if !request.disallowed_tools.is_empty() {
156                def = def.with_disallowed_tools(request.disallowed_tools.clone());
157            }
158            if let Some(max_turns) = request.max_turns {
159                def = def.with_max_turns(max_turns);
160            }
161            def
162        } else if let Some(registered) = self.registry.get(&request.name) {
163            // Use registered definition, with optional overrides
164            let mut def = registered.clone();
165            if let Some(max_turns) = request.max_turns {
166                def.max_turns = max_turns;
167            }
168            if !request.disallowed_tools.is_empty() {
169                def.disallowed_tools
170                    .extend(request.disallowed_tools.iter().cloned());
171            }
172            def
173        } else {
174            return Ok(json!({
175                "error": format!(
176                    "No subagent '{}' registered and no system_prompt provided for inline definition. \
177                    Available: {}",
178                    request.name,
179                    self.registry.list().iter().map(|d| d.name.as_str()).collect::<Vec<_>>().join(", ")
180                )
181            }));
182        };
183
184        let runner = SubAgentRunner::new(
185            self.work_dir.clone(),
186            self.source_root.clone(),
187            self.llm_client.clone(),
188        );
189        let runner = if let Some(ref tx) = self.event_tx {
190            runner.with_event_sink(tx.clone())
191        } else {
192            runner
193        };
194
195        if request.background || def.background {
196            // Background execution — return immediately, deliver results later.
197            // When background_tx is set the result is injected back into the
198            // parent agent's conversation (like Claude Code).  The event channel
199            // is always notified for CLI display.
200            let agent_id = Uuid::new_v4();
201            let handle = runner.run_background(def.clone(), request.prompt);
202
203            let event_tx = self.event_tx.clone();
204            let background_tx = self.background_tx.clone();
205            let name = def.name.clone();
206            tokio::spawn(async move {
207                match handle.await {
208                    Ok(Ok(result)) => {
209                        // Deliver result back to parent agent's conversation
210                        if let Some(bg_tx) = background_tx {
211                            let _ = bg_tx.send(BackgroundResult {
212                                name: result.name.clone(),
213                                kind: BackgroundResultKind::SubAgent,
214                                content: result.final_content.clone(),
215                                tokens_used: result.total_tokens,
216                            });
217                        }
218                        // Notify event listeners (CLI display)
219                        if let Some(tx) = event_tx {
220                            let _ = tx.send(AgentEvent::SubAgentCompleted {
221                                agent_id: result.agent_id,
222                                name: result.name,
223                                tokens_used: result.total_tokens,
224                                iterations: result.iterations,
225                                tool_calls: result.tool_calls_count,
226                                final_content: result.final_content,
227                            });
228                        }
229                    }
230                    Ok(Err(e)) => {
231                        if let Some(tx) = event_tx {
232                            let _ = tx.send(AgentEvent::SubAgentFailed {
233                                agent_id,
234                                name,
235                                error: e.to_string(),
236                            });
237                        }
238                    }
239                    Err(e) => {
240                        if let Some(tx) = event_tx {
241                            let _ = tx.send(AgentEvent::SubAgentFailed {
242                                agent_id,
243                                name,
244                                error: format!("Task join error: {}", e),
245                            });
246                        }
247                    }
248                }
249            });
250
251            Ok(json!({
252                "status": "background",
253                "agent_id": agent_id.to_string(),
254                "name": def.name,
255                "message": "Subagent started in background. You will be notified when it completes — continue with other work."
256            }))
257        } else {
258            // Foreground (blocking) execution
259            match runner.run(&def, &request.prompt).await {
260                Ok(result) => Ok(json!({
261                    "status": "completed",
262                    "name": result.name,
263                    "agent_id": result.agent_id.to_string(),
264                    "result": result.final_content,
265                    "total_tokens": result.total_tokens,
266                    "iterations": result.iterations,
267                    "tool_calls": result.tool_calls_count,
268                })),
269                Err(e) => Ok(json!({
270                    "status": "failed",
271                    "name": def.name,
272                    "error": e.to_string(),
273                })),
274            }
275        }
276    }
277}