1use acp_thread::SUBAGENT_SESSION_ID_META_KEY;
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)]
27pub struct SpawnAgentToolInput {
28 /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
29 pub label: String,
30 /// 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.
31 pub message: String,
32 /// Session ID of an existing agent session to continue instead of creating a new one.
33 #[serde(default)]
34 pub session_id: Option<acp::SessionId>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
38#[serde(untagged)]
39pub enum SpawnAgentToolOutput {
40 Success {
41 session_id: acp::SessionId,
42 output: String,
43 },
44 Error {
45 #[serde(skip_serializing_if = "Option::is_none")]
46 #[serde(default)]
47 session_id: Option<acp::SessionId>,
48 error: String,
49 },
50}
51
52impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
53 fn from(output: SpawnAgentToolOutput) -> Self {
54 serde_json::to_string(&output)
55 .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
56 .into()
57 }
58}
59
60/// Tool that spawns an agent thread to work on a task.
61pub struct SpawnAgentTool {
62 environment: Rc<dyn ThreadEnvironment>,
63}
64
65impl SpawnAgentTool {
66 pub fn new(environment: Rc<dyn ThreadEnvironment>) -> Self {
67 Self { environment }
68 }
69}
70
71impl AgentTool for SpawnAgentTool {
72 type Input = SpawnAgentToolInput;
73 type Output = SpawnAgentToolOutput;
74
75 const NAME: &'static str = "spawn_agent";
76
77 fn kind() -> acp::ToolKind {
78 acp::ToolKind::Other
79 }
80
81 fn initial_title(
82 &self,
83 input: Result<Self::Input, serde_json::Value>,
84 _cx: &mut App,
85 ) -> SharedString {
86 match input {
87 Ok(i) => i.label.into(),
88 Err(value) => value
89 .get("label")
90 .and_then(|v| v.as_str())
91 .map(|s| SharedString::from(s.to_owned()))
92 .unwrap_or_else(|| "Spawning agent".into()),
93 }
94 }
95
96 fn run(
97 self: Arc<Self>,
98 input: ToolInput<Self::Input>,
99 event_stream: ToolCallEventStream,
100 cx: &mut App,
101 ) -> Task<Result<Self::Output, Self::Output>> {
102 cx.spawn(async move |cx| {
103 let input = input
104 .recv()
105 .await
106 .map_err(|e| SpawnAgentToolOutput::Error {
107 session_id: None,
108 error: format!("Failed to receive tool input: {e}"),
109 })?;
110
111 let (subagent, subagent_session_id) = cx.update(|cx| {
112 let subagent = if let Some(session_id) = input.session_id {
113 self.environment.resume_subagent(session_id, cx)
114 } else {
115 self.environment.create_subagent(input.label, cx)
116 };
117 let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
118 session_id: None,
119 error: err.to_string(),
120 })?;
121 let subagent_session_id = subagent.id();
122
123 event_stream.subagent_spawned(subagent_session_id.clone());
124 let meta = acp::Meta::from_iter([(
125 SUBAGENT_SESSION_ID_META_KEY.into(),
126 subagent_session_id.to_string().into(),
127 )]);
128 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
129
130 Ok((subagent, subagent_session_id))
131 })?;
132
133 match subagent.send(input.message, cx).await {
134 Ok(output) => {
135 event_stream.update_fields(
136 acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
137 );
138 Ok(SpawnAgentToolOutput::Success {
139 session_id: subagent_session_id,
140 output,
141 })
142 }
143 Err(e) => {
144 let error = e.to_string();
145 // workaround for now because the agent loop will always mark this as ToolCallStatus::Failed
146 let canceled = error == "User canceled";
147 event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![
148 acp::ToolCallContent::Content(acp::Content::new(error.clone()).meta(
149 acp::Meta::from_iter([("cancelled".into(), canceled.into())]),
150 )),
151 ]));
152 Err(SpawnAgentToolOutput::Error {
153 session_id: Some(subagent_session_id),
154 error,
155 })
156 }
157 }
158 })
159 }
160
161 fn replay(
162 &self,
163 _input: Self::Input,
164 output: Self::Output,
165 event_stream: ToolCallEventStream,
166 _cx: &mut App,
167 ) -> Result<()> {
168 let session_id = match &output {
169 SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id),
170 SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(),
171 };
172
173 if let Some(session_id) = session_id {
174 event_stream.subagent_spawned(session_id.clone());
175 let meta = acp::Meta::from_iter([(
176 SUBAGENT_SESSION_ID_META_KEY.into(),
177 session_id.to_string().into(),
178 )]);
179 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
180 }
181
182 let content = match &output {
183 SpawnAgentToolOutput::Success { output, .. } => output.into(),
184 SpawnAgentToolOutput::Error { error, .. } => error.into(),
185 };
186 event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![content]));
187
188 Ok(())
189 }
190}