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