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};
 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/// Do NOT use this tool for simple tasks you could accomplish directly with one or two tool calls (e.g. reading a file, running a single command). Each agent has startup overhead.
 21///
 22/// 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.
 23///
 24/// You will receive only the agent's final message as output.
 25///
 26/// 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.
 27///
 28/// Note:
 29/// - Agents cannot use tools you don't have access to.
 30/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).
 31#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 32pub struct SpawnAgentToolInput {
 33    /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
 34    pub label: String,
 35    /// 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.
 36    pub message: String,
 37    /// 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.
 38    #[serde(default)]
 39    pub session_id: Option<acp::SessionId>,
 40}
 41
 42#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 43#[serde(untagged)]
 44pub enum SpawnAgentToolOutput {
 45    Success {
 46        session_id: acp::SessionId,
 47        output: String,
 48    },
 49    Error {
 50        #[serde(skip_serializing_if = "Option::is_none")]
 51        #[serde(default)]
 52        session_id: Option<acp::SessionId>,
 53        error: String,
 54    },
 55}
 56
 57impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
 58    fn from(output: SpawnAgentToolOutput) -> Self {
 59        serde_json::to_string(&output)
 60            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
 61            .into()
 62    }
 63}
 64
 65/// Tool that spawns an agent thread to work on a task.
 66pub struct SpawnAgentTool {
 67    parent_thread: WeakEntity<Thread>,
 68    environment: Rc<dyn ThreadEnvironment>,
 69}
 70
 71impl SpawnAgentTool {
 72    pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
 73        Self {
 74            parent_thread,
 75            environment,
 76        }
 77    }
 78}
 79
 80impl AgentTool for SpawnAgentTool {
 81    type Input = SpawnAgentToolInput;
 82    type Output = SpawnAgentToolOutput;
 83
 84    const NAME: &'static str = "spawn_agent";
 85
 86    fn kind() -> acp::ToolKind {
 87        acp::ToolKind::Other
 88    }
 89
 90    fn initial_title(
 91        &self,
 92        input: Result<Self::Input, serde_json::Value>,
 93        _cx: &mut App,
 94    ) -> SharedString {
 95        input
 96            .map(|i| i.label.into())
 97            .unwrap_or_else(|_| "Spawning agent".into())
 98    }
 99
100    fn run(
101        self: Arc<Self>,
102        input: Self::Input,
103        event_stream: ToolCallEventStream,
104        cx: &mut App,
105    ) -> Task<Result<Self::Output, Self::Output>> {
106        let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
107            return Task::ready(Err(SpawnAgentToolOutput::Error {
108                session_id: None,
109                error: "Parent thread no longer exists".to_string(),
110            }));
111        };
112
113        let subagent = if let Some(session_id) = input.session_id {
114            self.environment
115                .resume_subagent(parent_thread_entity, session_id, input.message, cx)
116        } else {
117            self.environment
118                .create_subagent(parent_thread_entity, input.label, input.message, cx)
119        };
120        let subagent = match subagent {
121            Ok(subagent) => subagent,
122            Err(err) => {
123                return Task::ready(Err(SpawnAgentToolOutput::Error {
124                    session_id: None,
125                    error: err.to_string(),
126                }));
127            }
128        };
129        let subagent_session_id = subagent.id();
130
131        event_stream.subagent_spawned(subagent_session_id.clone());
132        let meta = acp::Meta::from_iter([(
133            SUBAGENT_SESSION_ID_META_KEY.into(),
134            subagent_session_id.to_string().into(),
135        )]);
136        event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
137
138        cx.spawn(async move |cx| match subagent.wait_for_output(cx).await {
139            Ok(output) => {
140                event_stream.update_fields(
141                    acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
142                );
143                Ok(SpawnAgentToolOutput::Success {
144                    session_id: subagent_session_id,
145                    output,
146                })
147            }
148            Err(e) => {
149                let error = e.to_string();
150                event_stream.update_fields(
151                    acp::ToolCallUpdateFields::new().content(vec![error.clone().into()]),
152                );
153                Err(SpawnAgentToolOutput::Error {
154                    session_id: Some(subagent_session_id),
155                    error,
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}