1use acp_thread::SUBAGENT_SESSION_ID_META_KEY;
2use agent_client_protocol as acp;
3use anyhow::Result;
4use gpui::{App, 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 an agent 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 agent does by providing a prompt describing what the agent should do. The agent has access to the same tools you do.
21///
22/// You will receive the agent's final message.
23///
24/// Note:
25/// - Agents cannot use tools you don't have access to.
26/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories)
27#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
28pub struct SubagentToolInput {
29 /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
30 pub label: String,
31 /// The prompt that tells the agent what task to perform. Be specific about what you want the agent to accomplish.
32 pub prompt: String,
33 /// Optional: Maximum runtime in seconds. No timeout by default.
34 #[serde(default)]
35 pub timeout: Option<u64>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
39#[serde(untagged)]
40pub enum SubagentToolOutput {
41 Success {
42 session_id: acp::SessionId,
43 output: String,
44 },
45 Error {
46 error: String,
47 },
48}
49
50impl From<SubagentToolOutput> for LanguageModelToolResultContent {
51 fn from(output: SubagentToolOutput) -> Self {
52 match output {
53 output @ SubagentToolOutput::Success { .. } => serde_json::to_string(&output)
54 .unwrap_or_else(|e| format!("Failed to serialize subagent output: {e}"))
55 .into(),
56 SubagentToolOutput::Error { error } => error.into(),
57 }
58 }
59}
60
61/// Tool that spawns a subagent thread to work on a task.
62pub struct SubagentTool {
63 parent_thread: WeakEntity<Thread>,
64 environment: Rc<dyn ThreadEnvironment>,
65}
66
67impl SubagentTool {
68 pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
69 Self {
70 parent_thread,
71 environment,
72 }
73 }
74}
75
76impl AgentTool for SubagentTool {
77 type Input = SubagentToolInput;
78 type Output = SubagentToolOutput;
79
80 const NAME: &'static str = "subagent";
81
82 fn kind() -> acp::ToolKind {
83 acp::ToolKind::Other
84 }
85
86 fn initial_title(
87 &self,
88 input: Result<Self::Input, serde_json::Value>,
89 _cx: &mut App,
90 ) -> SharedString {
91 input
92 .map(|i| i.label.into())
93 .unwrap_or_else(|_| "Subagent".into())
94 }
95
96 fn run(
97 self: Arc<Self>,
98 input: Self::Input,
99 event_stream: ToolCallEventStream,
100 cx: &mut App,
101 ) -> Task<Result<Self::Output, Self::Output>> {
102 let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
103 return Task::ready(Err(SubagentToolOutput::Error {
104 error: "Parent thread no longer exists".to_string(),
105 }));
106 };
107
108 let subagent = match self.environment.create_subagent(
109 parent_thread_entity,
110 input.label,
111 input.prompt,
112 input.timeout.map(|secs| Duration::from_secs(secs)),
113 cx,
114 ) {
115 Ok(subagent) => subagent,
116 Err(err) => {
117 return Task::ready(Err(SubagentToolOutput::Error {
118 error: err.to_string(),
119 }));
120 }
121 };
122
123 let subagent_session_id = subagent.id();
124
125 event_stream.subagent_spawned(subagent_session_id.clone());
126 let meta = acp::Meta::from_iter([(
127 SUBAGENT_SESSION_ID_META_KEY.into(),
128 subagent_session_id.to_string().into(),
129 )]);
130 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
131
132 cx.spawn(async move |cx| {
133 let output =
134 subagent
135 .wait_for_output(cx)
136 .await
137 .map_err(|e| SubagentToolOutput::Error {
138 error: e.to_string(),
139 })?;
140 Ok(SubagentToolOutput::Success {
141 session_id: subagent_session_id,
142 output,
143 })
144 })
145 }
146
147 fn replay(
148 &self,
149 _input: Self::Input,
150 output: Self::Output,
151 event_stream: ToolCallEventStream,
152 _cx: &mut App,
153 ) -> Result<()> {
154 match output {
155 SubagentToolOutput::Success { session_id, .. } => {
156 event_stream.subagent_spawned(session_id.clone());
157 let meta = acp::Meta::from_iter([(
158 SUBAGENT_SESSION_ID_META_KEY.into(),
159 session_id.to_string().into(),
160 )]);
161 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
162 }
163 SubagentToolOutput::Error { .. } => {}
164 }
165 Ok(())
166 }
167}