1use acp_thread::SUBAGENT_SESSION_ID_META_KEY;
2use agent_client_protocol as acp;
3use anyhow::{Result, anyhow};
4use gpui::{App, Entity, SharedString, Task, WeakEntity};
5use language_model::LanguageModelToolResultContent;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use std::{rc::Rc, time::Duration};
10
11use crate::{AgentTool, Thread, ThreadEnvironment, ToolCallEventStream};
12
13/// Spawns a subagent with its own context window to perform a delegated task.
14///
15/// Use this tool when you want to do any of the following:
16/// - Perform an investigation where all you need to know is the outcome, not the research that led to that outcome.
17/// - Complete a self-contained task where you need to know if it succeeded or failed (and how), but none of its intermediate output.
18/// - Run multiple tasks in parallel that would take significantly longer to run sequentially.
19///
20/// You control what the subagent does by providing:
21/// 1. A task prompt describing what the subagent should do
22/// 2. A summary prompt that tells the subagent how to summarize its work when done
23/// 3. A "context running out" prompt for when the subagent is low on tokens
24///
25/// Each subagent has access to the same tools you do. You can optionally restrict
26/// which tools each subagent can use.
27///
28/// Note:
29/// - Maximum 8 subagents can run in parallel
30/// - Subagents cannot use tools you don't have access to
31/// - If spawning multiple subagents that might write to the filesystem, provide
32/// guidance on how to avoid conflicts (e.g. assign each to different directories)
33/// - Instruct subagents to be concise in their summaries to conserve your context
34#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
35pub struct SubagentToolInput {
36 /// Short label displayed in the UI while the subagent runs (e.g., "Researching alternatives")
37 pub label: String,
38
39 /// The initial prompt that tells the subagent what task to perform.
40 /// Be specific about what you want the subagent to accomplish.
41 pub task_prompt: String,
42
43 /// The prompt sent to the subagent when it completes its task, asking it
44 /// to summarize what it did and return results. This summary becomes the
45 /// tool result you receive.
46 ///
47 /// Example: "Summarize what you found, listing the top 3 alternatives with pros/cons."
48 pub summary_prompt: String,
49
50 /// Optional: Maximum runtime in milliseconds. If exceeded, the subagent is
51 /// asked to summarize and return. No timeout by default.
52 #[serde(default)]
53 pub timeout_ms: Option<u64>,
54
55 /// Optional: List of tool names the subagent is allowed to use.
56 /// If not provided, the subagent can use all tools available to the parent.
57 /// Tools listed here must be a subset of the parent's available tools.
58 #[serde(default)]
59 pub allowed_tools: Option<Vec<String>>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63pub struct SubagentToolOutput {
64 pub subagent_session_id: acp::SessionId,
65 pub summary: String,
66}
67
68impl From<SubagentToolOutput> for LanguageModelToolResultContent {
69 fn from(output: SubagentToolOutput) -> Self {
70 output.summary.into()
71 }
72}
73
74/// Tool that spawns a subagent thread to work on a task.
75pub struct SubagentTool {
76 parent_thread: WeakEntity<Thread>,
77 environment: Rc<dyn ThreadEnvironment>,
78}
79
80impl SubagentTool {
81 pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
82 Self {
83 parent_thread,
84 environment,
85 }
86 }
87
88 fn validate_allowed_tools(
89 allowed_tools: &Option<Vec<String>>,
90 parent_thread: &Entity<Thread>,
91 cx: &App,
92 ) -> Result<()> {
93 let Some(allowed_tools) = allowed_tools else {
94 return Ok(());
95 };
96
97 let thread = parent_thread.read(cx);
98 let invalid_tools: Vec<_> = allowed_tools
99 .iter()
100 .filter(|tool| !thread.tools.contains_key(tool.as_str()))
101 .map(|s| format!("'{s}'"))
102 .collect::<Vec<_>>();
103
104 if !invalid_tools.is_empty() {
105 return Err(anyhow!(
106 "The following tools do not exist: {}",
107 invalid_tools.join(", ")
108 ));
109 }
110
111 Ok(())
112 }
113}
114
115impl AgentTool for SubagentTool {
116 type Input = SubagentToolInput;
117 type Output = SubagentToolOutput;
118
119 const NAME: &'static str = "subagent";
120
121 fn kind() -> acp::ToolKind {
122 acp::ToolKind::Other
123 }
124
125 fn initial_title(
126 &self,
127 input: Result<Self::Input, serde_json::Value>,
128 _cx: &mut App,
129 ) -> SharedString {
130 input
131 .map(|i| i.label.into())
132 .unwrap_or_else(|_| "Subagent".into())
133 }
134
135 fn run(
136 self: Arc<Self>,
137 input: Self::Input,
138 event_stream: ToolCallEventStream,
139 cx: &mut App,
140 ) -> Task<Result<SubagentToolOutput>> {
141 let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
142 return Task::ready(Err(anyhow!("Parent thread no longer exists")));
143 };
144
145 if let Err(e) =
146 Self::validate_allowed_tools(&input.allowed_tools, &parent_thread_entity, cx)
147 {
148 return Task::ready(Err(e));
149 }
150
151 let subagent = match self.environment.create_subagent(
152 parent_thread_entity,
153 input.label,
154 input.task_prompt,
155 input.timeout_ms.map(|ms| Duration::from_millis(ms)),
156 input.allowed_tools,
157 cx,
158 ) {
159 Ok(subagent) => subagent,
160 Err(err) => return Task::ready(Err(err)),
161 };
162
163 let subagent_session_id = subagent.id();
164
165 event_stream.subagent_spawned(subagent_session_id.clone());
166 let meta = acp::Meta::from_iter([(
167 SUBAGENT_SESSION_ID_META_KEY.into(),
168 subagent_session_id.to_string().into(),
169 )]);
170 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
171
172 cx.spawn(async move |cx| {
173 let summary = subagent.wait_for_summary(input.summary_prompt, cx).await?;
174 Ok(SubagentToolOutput {
175 subagent_session_id,
176 summary,
177 })
178 })
179 }
180
181 fn replay(
182 &self,
183 _input: Self::Input,
184 output: Self::Output,
185 event_stream: ToolCallEventStream,
186 _cx: &mut App,
187 ) -> Result<()> {
188 event_stream.subagent_spawned(output.subagent_session_id.clone());
189 let meta = acp::Meta::from_iter([(
190 SUBAGENT_SESSION_ID_META_KEY.into(),
191 output.subagent_session_id.to_string().into(),
192 )]);
193 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
194 Ok(())
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::{ContextServerRegistry, Templates, Thread};
202 use fs::FakeFs;
203 use gpui::{AppContext as _, TestAppContext};
204 use project::Project;
205 use prompt_store::ProjectContext;
206 use serde_json::json;
207 use settings::SettingsStore;
208 use util::path;
209
210 async fn create_thread_with_tools(cx: &mut TestAppContext) -> Entity<Thread> {
211 cx.update(|cx| {
212 let settings_store = SettingsStore::test(cx);
213 cx.set_global(settings_store);
214 });
215 let fs = FakeFs::new(cx.executor());
216 fs.insert_tree(path!("/test"), json!({})).await;
217 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
218 let project_context = cx.new(|_cx| ProjectContext::default());
219 let context_server_store =
220 project.read_with(cx, |project, _| project.context_server_store());
221 let context_server_registry =
222 cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
223
224 cx.new(|cx| {
225 let mut thread = Thread::new(
226 project,
227 project_context,
228 context_server_registry,
229 Templates::new(),
230 None,
231 cx,
232 );
233 thread.add_tool(crate::NowTool, None);
234 thread.add_tool(crate::WebSearchTool, None);
235 thread
236 })
237 }
238
239 #[gpui::test]
240 async fn test_validate_allowed_tools_succeeds_for_valid_tools(cx: &mut TestAppContext) {
241 let thread = create_thread_with_tools(cx).await;
242
243 cx.update(|cx| {
244 assert!(SubagentTool::validate_allowed_tools(&None, &thread, cx).is_ok());
245
246 let valid_tools = Some(vec!["now".to_string()]);
247 assert!(SubagentTool::validate_allowed_tools(&valid_tools, &thread, cx).is_ok());
248
249 let both_tools = Some(vec!["now".to_string(), "web_search".to_string()]);
250 assert!(SubagentTool::validate_allowed_tools(&both_tools, &thread, cx).is_ok());
251 });
252 }
253
254 #[gpui::test]
255 async fn test_validate_allowed_tools_fails_for_unknown_tools(cx: &mut TestAppContext) {
256 let thread = create_thread_with_tools(cx).await;
257
258 cx.update(|cx| {
259 let unknown_tools = Some(vec!["nonexistent_tool".to_string()]);
260 let result = SubagentTool::validate_allowed_tools(&unknown_tools, &thread, cx);
261 assert!(result.is_err());
262 let error_message = result.unwrap_err().to_string();
263 assert!(
264 error_message.contains("'nonexistent_tool'"),
265 "Expected error to mention the invalid tool name, got: {error_message}"
266 );
267
268 let mixed_tools = Some(vec![
269 "now".to_string(),
270 "fake_tool_a".to_string(),
271 "fake_tool_b".to_string(),
272 ]);
273 let result = SubagentTool::validate_allowed_tools(&mixed_tools, &thread, cx);
274 assert!(result.is_err());
275 let error_message = result.unwrap_err().to_string();
276 assert!(
277 error_message.contains("'fake_tool_a'") && error_message.contains("'fake_tool_b'"),
278 "Expected error to mention both invalid tool names, got: {error_message}"
279 );
280 assert!(
281 !error_message.contains("'now'"),
282 "Expected error to not mention valid tool 'now', got: {error_message}"
283 );
284 });
285 }
286}