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::sync::Arc;
  9use std::{rc::Rc, time::Duration};
 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    /// Optional maximum runtime in seconds. The purpose of this timeout is to prevent the agent from getting stuck in infinite loops, NOT to estimate task duration. Be generous if setting. If not set, the agent runs until it completes or is cancelled.
 41    #[serde(default)]
 42    pub timeout_secs: Option<u64>,
 43}
 44
 45#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 46#[serde(untagged)]
 47pub enum SpawnAgentToolOutput {
 48    Success {
 49        session_id: acp::SessionId,
 50        output: String,
 51    },
 52    Error {
 53        #[serde(skip_serializing_if = "Option::is_none")]
 54        #[serde(default)]
 55        session_id: Option<acp::SessionId>,
 56        error: String,
 57    },
 58}
 59
 60impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
 61    fn from(output: SpawnAgentToolOutput) -> Self {
 62        serde_json::to_string(&output)
 63            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
 64            .into()
 65    }
 66}
 67
 68/// Tool that spawns an agent thread to work on a task.
 69pub struct SpawnAgentTool {
 70    parent_thread: WeakEntity<Thread>,
 71    environment: Rc<dyn ThreadEnvironment>,
 72}
 73
 74impl SpawnAgentTool {
 75    pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
 76        Self {
 77            parent_thread,
 78            environment,
 79        }
 80    }
 81}
 82
 83impl AgentTool for SpawnAgentTool {
 84    type Input = SpawnAgentToolInput;
 85    type Output = SpawnAgentToolOutput;
 86
 87    const NAME: &'static str = "spawn_agent";
 88
 89    fn kind() -> acp::ToolKind {
 90        acp::ToolKind::Other
 91    }
 92
 93    fn initial_title(
 94        &self,
 95        input: Result<Self::Input, serde_json::Value>,
 96        _cx: &mut App,
 97    ) -> SharedString {
 98        input
 99            .map(|i| i.label.into())
100            .unwrap_or_else(|_| "Spawning agent".into())
101    }
102
103    fn run(
104        self: Arc<Self>,
105        input: Self::Input,
106        event_stream: ToolCallEventStream,
107        cx: &mut App,
108    ) -> Task<Result<Self::Output, Self::Output>> {
109        let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
110            return Task::ready(Err(SpawnAgentToolOutput::Error {
111                session_id: None,
112                error: "Parent thread no longer exists".to_string(),
113            }));
114        };
115
116        let subagent = if let Some(session_id) = input.session_id {
117            self.environment.resume_subagent(
118                parent_thread_entity,
119                session_id,
120                input.message,
121                input.timeout_secs.map(Duration::from_secs),
122                cx,
123            )
124        } else {
125            self.environment.create_subagent(
126                parent_thread_entity,
127                input.label,
128                input.message,
129                input.timeout_secs.map(Duration::from_secs),
130                cx,
131            )
132        };
133        let subagent = match subagent {
134            Ok(subagent) => subagent,
135            Err(err) => {
136                return Task::ready(Err(SpawnAgentToolOutput::Error {
137                    session_id: None,
138                    error: err.to_string(),
139                }));
140            }
141        };
142        let subagent_session_id = subagent.id();
143
144        event_stream.subagent_spawned(subagent_session_id.clone());
145        let meta = acp::Meta::from_iter([(
146            SUBAGENT_SESSION_ID_META_KEY.into(),
147            subagent_session_id.to_string().into(),
148        )]);
149        event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
150
151        cx.spawn(async move |cx| match subagent.wait_for_output(cx).await {
152            Ok(output) => {
153                event_stream.update_fields(
154                    acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
155                );
156                Ok(SpawnAgentToolOutput::Success {
157                    session_id: subagent_session_id,
158                    output,
159                })
160            }
161            Err(e) => {
162                let error = e.to_string();
163                event_stream.update_fields(
164                    acp::ToolCallUpdateFields::new().content(vec![error.clone().into()]),
165                );
166                Err(SpawnAgentToolOutput::Error {
167                    session_id: Some(subagent_session_id),
168                    error,
169                })
170            }
171        })
172    }
173
174    fn replay(
175        &self,
176        _input: Self::Input,
177        output: Self::Output,
178        event_stream: ToolCallEventStream,
179        _cx: &mut App,
180    ) -> Result<()> {
181        let session_id = match &output {
182            SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id),
183            SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(),
184        };
185
186        if let Some(session_id) = session_id {
187            event_stream.subagent_spawned(session_id.clone());
188            let meta = acp::Meta::from_iter([(
189                SUBAGENT_SESSION_ID_META_KEY.into(),
190                session_id.to_string().into(),
191            )]);
192            event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
193        }
194
195        let content = match &output {
196            SpawnAgentToolOutput::Success { output, .. } => output.into(),
197            SpawnAgentToolOutput::Error { error, .. } => error.into(),
198        };
199        event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![content]));
200
201        Ok(())
202    }
203}