spawn_agent_tool.rs

  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}