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