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
16pub 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 pub background_tx: Option<tokio::sync::mpsc::UnboundedSender<BackgroundResult>>,
32}
33
34#[derive(Debug, Deserialize)]
35struct SubAgentRequest {
36 name: String,
38 prompt: String,
40 #[serde(default)]
43 system_prompt: Option<String>,
44 #[serde(default)]
46 description: Option<String>,
47 #[serde(default)]
49 allowed_tools: Vec<String>,
50 #[serde(default)]
52 disallowed_tools: Vec<String>,
53 #[serde(default)]
55 max_turns: Option<usize>,
56 #[serde(default)]
58 background: bool,
59}
60
61#[async_trait]
62impl Tool for SpawnSubAgentTool {
63 fn definition(&self) -> ToolDefinition {
64 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 let def = if let Some(ref system_prompt) = request.system_prompt {
146 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 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 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 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 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 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}