subagent_tool.rs

  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}