spawn_agent_tool.rs

  1use acp_thread::SUBAGENT_SESSION_ID_META_KEY;
  2use agent_client_protocol as acp;
  3use anyhow::Result;
  4use gpui::{App, SharedString, Task, WeakEntity};
  5use language_model::LanguageModelToolResultContent;
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use std::rc::Rc;
  9use std::sync::Arc;
 10
 11use crate::{AgentTool, Thread, ThreadEnvironment, ToolCallEventStream, ToolInput};
 12
 13/// Spawns an agent to perform a delegated task.
 14///
 15/// Use this tool when you want to do any of the following:
 16/// - Run multiple tasks in parallel that would take significantly longer to run sequentially.
 17/// - Complete a self-contained task where you need to know if it succeeded or failed (and how), but none of its intermediate output.
 18/// - Perform an investigation where all you need to know is the outcome, not the research that led to that outcome.
 19///
 20/// You control what the agent does by providing a prompt describing what the agent should do. The agent has access to the same tools you do, but does NOT see your conversation history or any context the user attached. You must include all relevant context (file paths, requirements, constraints) in the prompt.
 21///
 22/// You will receive only the agent's final message as output.
 23///
 24/// If a response (success or error) includes a session_id, you can send a follow-up message to that session by passing the session_id back. This is useful for multi-turn conversations with an agent, asking clarifying questions about its output, or retrying after timeouts or transient failures.
 25///
 26/// Note:
 27/// - Agents cannot use tools you don't have access to.
 28/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).
 29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 30pub struct SpawnAgentToolInput {
 31    /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
 32    pub label: String,
 33    /// Describe the task for the agent to perform. Be specific about what you want accomplished. Include all necessary context (file paths, requirements, constraints) since the agent cannot see your conversation.
 34    pub message: String,
 35    /// Optional session ID of an existing agent session to continue a conversation with. When provided, the message is sent as a follow-up to that session instead of creating a new one. Use this to ask clarifying questions, request changes based on previous output, or retry after errors.
 36    #[serde(default)]
 37    pub session_id: Option<acp::SessionId>,
 38}
 39
 40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 41#[serde(untagged)]
 42pub enum SpawnAgentToolOutput {
 43    Success {
 44        session_id: acp::SessionId,
 45        output: String,
 46    },
 47    Error {
 48        #[serde(skip_serializing_if = "Option::is_none")]
 49        #[serde(default)]
 50        session_id: Option<acp::SessionId>,
 51        error: String,
 52    },
 53}
 54
 55impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
 56    fn from(output: SpawnAgentToolOutput) -> Self {
 57        serde_json::to_string(&output)
 58            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
 59            .into()
 60    }
 61}
 62
 63/// Tool that spawns an agent thread to work on a task.
 64pub struct SpawnAgentTool {
 65    parent_thread: WeakEntity<Thread>,
 66    environment: Rc<dyn ThreadEnvironment>,
 67}
 68
 69impl SpawnAgentTool {
 70    pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
 71        Self {
 72            parent_thread,
 73            environment,
 74        }
 75    }
 76}
 77
 78impl AgentTool for SpawnAgentTool {
 79    type Input = SpawnAgentToolInput;
 80    type Output = SpawnAgentToolOutput;
 81
 82    const NAME: &'static str = "spawn_agent";
 83
 84    fn kind() -> acp::ToolKind {
 85        acp::ToolKind::Other
 86    }
 87
 88    fn initial_title(
 89        &self,
 90        input: Result<Self::Input, serde_json::Value>,
 91        _cx: &mut App,
 92    ) -> SharedString {
 93        input
 94            .map(|i| i.label.into())
 95            .unwrap_or_else(|_| "Spawning agent".into())
 96    }
 97
 98    fn run(
 99        self: Arc<Self>,
100        input: ToolInput<Self::Input>,
101        event_stream: ToolCallEventStream,
102        cx: &mut App,
103    ) -> Task<Result<Self::Output, Self::Output>> {
104        cx.spawn(async move |cx| {
105            let input = input
106                .recv()
107                .await
108                .map_err(|e| SpawnAgentToolOutput::Error {
109                    session_id: None,
110                    error: format!("Failed to receive tool input: {e}"),
111                })?;
112
113            let (subagent, subagent_session_id) = cx.update(|cx| {
114                let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
115                    return Err(SpawnAgentToolOutput::Error {
116                        session_id: None,
117                        error: "Parent thread no longer exists".to_string(),
118                    });
119                };
120
121                let subagent = if let Some(session_id) = input.session_id {
122                    self.environment.resume_subagent(
123                        parent_thread_entity,
124                        session_id,
125                        input.message,
126                        cx,
127                    )
128                } else {
129                    self.environment.create_subagent(
130                        parent_thread_entity,
131                        input.label,
132                        input.message,
133                        cx,
134                    )
135                };
136                let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
137                    session_id: None,
138                    error: err.to_string(),
139                })?;
140                let subagent_session_id = subagent.id();
141
142                event_stream.subagent_spawned(subagent_session_id.clone());
143                let meta = acp::Meta::from_iter([(
144                    SUBAGENT_SESSION_ID_META_KEY.into(),
145                    subagent_session_id.to_string().into(),
146                )]);
147                event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
148
149                Ok((subagent, subagent_session_id))
150            })?;
151
152            match subagent.wait_for_output(cx).await {
153                Ok(output) => {
154                    event_stream.update_fields(
155                        acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
156                    );
157                    Ok(SpawnAgentToolOutput::Success {
158                        session_id: subagent_session_id,
159                        output,
160                    })
161                }
162                Err(e) => {
163                    let error = e.to_string();
164                    event_stream.update_fields(
165                        acp::ToolCallUpdateFields::new().content(vec![error.clone().into()]),
166                    );
167                    Err(SpawnAgentToolOutput::Error {
168                        session_id: Some(subagent_session_id),
169                        error,
170                    })
171                }
172            }
173        })
174    }
175
176    fn replay(
177        &self,
178        _input: Self::Input,
179        output: Self::Output,
180        event_stream: ToolCallEventStream,
181        _cx: &mut App,
182    ) -> Result<()> {
183        let session_id = match &output {
184            SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id),
185            SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(),
186        };
187
188        if let Some(session_id) = session_id {
189            event_stream.subagent_spawned(session_id.clone());
190            let meta = acp::Meta::from_iter([(
191                SUBAGENT_SESSION_ID_META_KEY.into(),
192                session_id.to_string().into(),
193            )]);
194            event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
195        }
196
197        let content = match &output {
198            SpawnAgentToolOutput::Success { output, .. } => output.into(),
199            SpawnAgentToolOutput::Error { error, .. } => error.into(),
200        };
201        event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![content]));
202
203        Ok(())
204    }
205}