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/// You will receive only the agent's final message as output.
20///
21/// **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.
22///
23/// **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".
24///
25/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).
26#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
27#[serde(rename_all = "snake_case")]
28pub struct SpawnAgentToolInput {
29 /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
30 pub label: String,
31 /// 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.
32 pub message: String,
33 /// Session ID of an existing agent session to continue instead of creating a new one.
34 #[serde(default)]
35 pub session_id: Option<acp::SessionId>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(untagged)]
40#[serde(rename_all = "snake_case")]
41pub enum SpawnAgentToolOutput {
42 Success {
43 session_id: acp::SessionId,
44 output: String,
45 session_info: SubagentSessionInfo,
46 },
47 Error {
48 #[serde(skip_serializing_if = "Option::is_none")]
49 #[serde(default)]
50 session_id: Option<acp::SessionId>,
51 error: String,
52 session_info: Option<SubagentSessionInfo>,
53 },
54}
55
56impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
57 fn from(output: SpawnAgentToolOutput) -> Self {
58 match output {
59 SpawnAgentToolOutput::Success {
60 session_id,
61 output,
62 session_info: _, // Don't show this to the model
63 } => serde_json::to_string(
64 &serde_json::json!({ "session_id": session_id, "output": output }),
65 )
66 .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
67 .into(),
68 SpawnAgentToolOutput::Error {
69 session_id,
70 error,
71 session_info: _, // Don't show this to the model
72 } => serde_json::to_string(
73 &serde_json::json!({ "session_id": session_id, "error": error }),
74 )
75 .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
76 .into(),
77 }
78 }
79}
80
81/// Tool that spawns an agent thread to work on a task.
82pub struct SpawnAgentTool {
83 environment: Rc<dyn ThreadEnvironment>,
84}
85
86impl SpawnAgentTool {
87 pub fn new(environment: Rc<dyn ThreadEnvironment>) -> Self {
88 Self { environment }
89 }
90}
91
92impl AgentTool for SpawnAgentTool {
93 type Input = SpawnAgentToolInput;
94 type Output = SpawnAgentToolOutput;
95
96 const NAME: &'static str = "spawn_agent";
97
98 fn kind() -> acp::ToolKind {
99 acp::ToolKind::Other
100 }
101
102 fn initial_title(
103 &self,
104 input: Result<Self::Input, serde_json::Value>,
105 _cx: &mut App,
106 ) -> SharedString {
107 match input {
108 Ok(i) => i.label.into(),
109 Err(value) => value
110 .get("label")
111 .and_then(|v| v.as_str())
112 .map(|s| SharedString::from(s.to_owned()))
113 .unwrap_or_else(|| "Spawning agent".into()),
114 }
115 }
116
117 fn run(
118 self: Arc<Self>,
119 input: ToolInput<Self::Input>,
120 event_stream: ToolCallEventStream,
121 cx: &mut App,
122 ) -> Task<Result<Self::Output, Self::Output>> {
123 cx.spawn(async move |cx| {
124 let input = input
125 .recv()
126 .await
127 .map_err(|e| SpawnAgentToolOutput::Error {
128 session_id: None,
129 error: format!("Failed to receive tool input: {e}"),
130 session_info: None,
131 })?;
132
133 let (subagent, mut session_info) = cx.update(|cx| {
134 let subagent = if let Some(session_id) = input.session_id {
135 self.environment.resume_subagent(session_id, cx)
136 } else {
137 self.environment.create_subagent(input.label, cx)
138 };
139 let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
140 session_id: None,
141 error: err.to_string(),
142 session_info: None,
143 })?;
144 let session_info = SubagentSessionInfo {
145 session_id: subagent.id(),
146 message_start_index: subagent.num_entries(cx),
147 message_end_index: None,
148 };
149
150 event_stream.subagent_spawned(subagent.id());
151 event_stream.update_fields_with_meta(
152 acp::ToolCallUpdateFields::new(),
153 Some(acp::Meta::from_iter([(
154 SUBAGENT_SESSION_INFO_META_KEY.into(),
155 serde_json::json!(&session_info),
156 )])),
157 );
158
159 Ok((subagent, session_info))
160 })?;
161
162 match subagent.send(input.message, cx).await {
163 Ok(output) => {
164 session_info.message_end_index =
165 cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1)));
166 event_stream.update_fields_with_meta(
167 acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
168 Some(acp::Meta::from_iter([(
169 SUBAGENT_SESSION_INFO_META_KEY.into(),
170 serde_json::json!(&session_info),
171 )])),
172 );
173 Ok(SpawnAgentToolOutput::Success {
174 session_id: session_info.session_id.clone(),
175 session_info,
176 output,
177 })
178 }
179 Err(e) => Err(SpawnAgentToolOutput::Error {
180 session_id: Some(session_info.session_id.clone()),
181 error: e.to_string(),
182 session_info: Some(session_info),
183 }),
184 }
185 })
186 }
187
188 fn replay(
189 &self,
190 _input: Self::Input,
191 output: Self::Output,
192 event_stream: ToolCallEventStream,
193 _cx: &mut App,
194 ) -> Result<()> {
195 let (content, session_info) = match output {
196 SpawnAgentToolOutput::Success {
197 output,
198 session_info,
199 ..
200 } => (output.into(), Some(session_info)),
201 SpawnAgentToolOutput::Error {
202 error,
203 session_info,
204 ..
205 } => (error.into(), session_info),
206 };
207
208 let meta = session_info.map(|session_info| {
209 acp::Meta::from_iter([(
210 SUBAGENT_SESSION_INFO_META_KEY.into(),
211 serde_json::json!(&session_info),
212 )])
213 });
214 event_stream.update_fields_with_meta(
215 acp::ToolCallUpdateFields::new().content(vec![content]),
216 meta,
217 );
218
219 Ok(())
220 }
221}