1use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo};
2use agent_client_protocol as acp;
3use anyhow::Result;
4use gpui::{App, SharedString, Task};
5use language_model::LanguageModelToolResultContent;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::rc::Rc;
9use std::sync::Arc;
10
11use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream, ToolInput};
12
13/// Spawns an agent to perform a delegated task.
14///
15/// Use this tool when you want to:
16/// - Run multiple tasks in parallel.
17/// - Delegate a self-contained task where you only need the final outcome.
18///
19/// Do NOT use this tool for tasks you could accomplish directly with one or two tool calls (e.g. reading a file, running a single command).
20///
21/// You will receive only the agent's final message as output.
22///
23/// **New session** (no session_id): Creates a new agent that does NOT see your conversation history. Include all relevant context (file paths, requirements, constraints) in the message.
24///
25/// **Follow-up** (with session_id): Sends a follow-up to an existing agent session. The agent already has full context, so send only a short, direct message — do NOT repeat the original task or context. Examples: "Also update the tests", "Fix the compile error in foo.rs", "Retry".
26///
27/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
29#[serde(rename_all = "snake_case")]
30pub struct SpawnAgentToolInput {
31 /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
32 pub label: String,
33 /// The prompt for the agent. For new sessions, include full context needed for the task. For follow-ups (with session_id), you can rely on the agent already having the previous message.
34 pub message: String,
35 /// Session ID of an existing agent session to continue instead of creating a new one.
36 #[serde(default)]
37 pub session_id: Option<acp::SessionId>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(untagged)]
42#[serde(rename_all = "snake_case")]
43pub enum SpawnAgentToolOutput {
44 Success {
45 session_id: acp::SessionId,
46 output: String,
47 session_info: SubagentSessionInfo,
48 },
49 Error {
50 #[serde(skip_serializing_if = "Option::is_none")]
51 #[serde(default)]
52 session_id: Option<acp::SessionId>,
53 error: String,
54 session_info: Option<SubagentSessionInfo>,
55 },
56}
57
58impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
59 fn from(output: SpawnAgentToolOutput) -> Self {
60 match output {
61 SpawnAgentToolOutput::Success {
62 session_id,
63 output,
64 session_info: _, // Don't show this to the model
65 } => serde_json::to_string(
66 &serde_json::json!({ "session_id": session_id, "output": output }),
67 )
68 .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
69 .into(),
70 SpawnAgentToolOutput::Error {
71 session_id,
72 error,
73 session_info: _, // Don't show this to the model
74 } => serde_json::to_string(
75 &serde_json::json!({ "session_id": session_id, "error": error }),
76 )
77 .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
78 .into(),
79 }
80 }
81}
82
83/// Tool that spawns an agent thread to work on a task.
84pub struct SpawnAgentTool {
85 environment: Rc<dyn ThreadEnvironment>,
86}
87
88impl SpawnAgentTool {
89 pub fn new(environment: Rc<dyn ThreadEnvironment>) -> Self {
90 Self { environment }
91 }
92}
93
94impl AgentTool for SpawnAgentTool {
95 type Input = SpawnAgentToolInput;
96 type Output = SpawnAgentToolOutput;
97
98 const NAME: &'static str = "spawn_agent";
99
100 fn kind() -> acp::ToolKind {
101 acp::ToolKind::Other
102 }
103
104 fn initial_title(
105 &self,
106 input: Result<Self::Input, serde_json::Value>,
107 _cx: &mut App,
108 ) -> SharedString {
109 match input {
110 Ok(i) => i.label.into(),
111 Err(value) => value
112 .get("label")
113 .and_then(|v| v.as_str())
114 .map(|s| SharedString::from(s.to_owned()))
115 .unwrap_or_else(|| "Spawning agent".into()),
116 }
117 }
118
119 fn run(
120 self: Arc<Self>,
121 input: ToolInput<Self::Input>,
122 event_stream: ToolCallEventStream,
123 cx: &mut App,
124 ) -> Task<Result<Self::Output, Self::Output>> {
125 cx.spawn(async move |cx| {
126 let input = input
127 .recv()
128 .await
129 .map_err(|e| SpawnAgentToolOutput::Error {
130 session_id: None,
131 error: format!("Failed to receive tool input: {e}"),
132 session_info: None,
133 })?;
134
135 let (subagent, mut session_info) = cx.update(|cx| {
136 let subagent = if let Some(session_id) = input.session_id {
137 self.environment.resume_subagent(session_id, cx)
138 } else {
139 self.environment.create_subagent(input.label, cx)
140 };
141 let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
142 session_id: None,
143 error: err.to_string(),
144 session_info: None,
145 })?;
146 let session_info = SubagentSessionInfo {
147 session_id: subagent.id(),
148 message_start_index: subagent.num_entries(cx),
149 message_end_index: None,
150 };
151
152 event_stream.subagent_spawned(subagent.id());
153 event_stream.update_fields_with_meta(
154 acp::ToolCallUpdateFields::new(),
155 Some(acp::Meta::from_iter([(
156 SUBAGENT_SESSION_INFO_META_KEY.into(),
157 serde_json::json!(&session_info),
158 )])),
159 );
160
161 Ok((subagent, session_info))
162 })?;
163
164 let send_result = subagent.send(input.message, cx).await;
165
166 let status = if send_result.is_ok() {
167 "completed"
168 } else {
169 "error"
170 };
171 telemetry::event!(
172 "Subagent Completed",
173 subagent_session = session_info.session_id.to_string(),
174 status,
175 );
176
177 session_info.message_end_index =
178 cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1)));
179
180 let meta = Some(acp::Meta::from_iter([(
181 SUBAGENT_SESSION_INFO_META_KEY.into(),
182 serde_json::json!(&session_info),
183 )]));
184
185 let (output, result) = match send_result {
186 Ok(output) => (
187 output.clone(),
188 Ok(SpawnAgentToolOutput::Success {
189 session_id: session_info.session_id.clone(),
190 session_info,
191 output,
192 }),
193 ),
194 Err(e) => {
195 let error = e.to_string();
196 (
197 error.clone(),
198 Err(SpawnAgentToolOutput::Error {
199 session_id: Some(session_info.session_id.clone()),
200 error,
201 session_info: Some(session_info),
202 }),
203 )
204 }
205 };
206 event_stream.update_fields_with_meta(
207 acp::ToolCallUpdateFields::new().content(vec![output.into()]),
208 meta,
209 );
210 result
211 })
212 }
213
214 fn replay(
215 &self,
216 _input: Self::Input,
217 output: Self::Output,
218 event_stream: ToolCallEventStream,
219 _cx: &mut App,
220 ) -> Result<()> {
221 let (content, session_info) = match output {
222 SpawnAgentToolOutput::Success {
223 output,
224 session_info,
225 ..
226 } => (output.into(), Some(session_info)),
227 SpawnAgentToolOutput::Error {
228 error,
229 session_info,
230 ..
231 } => (error.into(), session_info),
232 };
233
234 let meta = session_info.map(|session_info| {
235 acp::Meta::from_iter([(
236 SUBAGENT_SESSION_INFO_META_KEY.into(),
237 serde_json::json!(&session_info),
238 )])
239 });
240 event_stream.update_fields_with_meta(
241 acp::ToolCallUpdateFields::new().content(vec![content]),
242 meta,
243 );
244
245 Ok(())
246 }
247}