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 session_info.message_end_index =
167 cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1)));
168
169 let meta = Some(acp::Meta::from_iter([(
170 SUBAGENT_SESSION_INFO_META_KEY.into(),
171 serde_json::json!(&session_info),
172 )]));
173
174 let (output, result) = match send_result {
175 Ok(output) => (
176 output.clone(),
177 Ok(SpawnAgentToolOutput::Success {
178 session_id: session_info.session_id.clone(),
179 session_info,
180 output,
181 }),
182 ),
183 Err(e) => {
184 let error = e.to_string();
185 (
186 error.clone(),
187 Err(SpawnAgentToolOutput::Error {
188 session_id: Some(session_info.session_id.clone()),
189 error,
190 session_info: Some(session_info),
191 }),
192 )
193 }
194 };
195 event_stream.update_fields_with_meta(
196 acp::ToolCallUpdateFields::new().content(vec![output.into()]),
197 meta,
198 );
199 result
200 })
201 }
202
203 fn replay(
204 &self,
205 _input: Self::Input,
206 output: Self::Output,
207 event_stream: ToolCallEventStream,
208 _cx: &mut App,
209 ) -> Result<()> {
210 let (content, session_info) = match output {
211 SpawnAgentToolOutput::Success {
212 output,
213 session_info,
214 ..
215 } => (output.into(), Some(session_info)),
216 SpawnAgentToolOutput::Error {
217 error,
218 session_info,
219 ..
220 } => (error.into(), session_info),
221 };
222
223 let meta = session_info.map(|session_info| {
224 acp::Meta::from_iter([(
225 SUBAGENT_SESSION_INFO_META_KEY.into(),
226 serde_json::json!(&session_info),
227 )])
228 });
229 event_stream.update_fields_with_meta(
230 acp::ToolCallUpdateFields::new().content(vec![content]),
231 meta,
232 );
233
234 Ok(())
235 }
236}