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:
 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    parent_thread: WeakEntity<Thread>,
 63    environment: Rc<dyn ThreadEnvironment>,
 64}
 65
 66impl SpawnAgentTool {
 67    pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
 68        Self {
 69            parent_thread,
 70            environment,
 71        }
 72    }
 73}
 74
 75impl AgentTool for SpawnAgentTool {
 76    type Input = SpawnAgentToolInput;
 77    type Output = SpawnAgentToolOutput;
 78
 79    const NAME: &'static str = "spawn_agent";
 80
 81    fn kind() -> acp::ToolKind {
 82        acp::ToolKind::Other
 83    }
 84
 85    fn initial_title(
 86        &self,
 87        input: Result<Self::Input, serde_json::Value>,
 88        _cx: &mut App,
 89    ) -> SharedString {
 90        input
 91            .map(|i| i.label.into())
 92            .unwrap_or_else(|_| "Spawning agent".into())
 93    }
 94
 95    fn run(
 96        self: Arc<Self>,
 97        input: ToolInput<Self::Input>,
 98        event_stream: ToolCallEventStream,
 99        cx: &mut App,
100    ) -> Task<Result<Self::Output, Self::Output>> {
101        cx.spawn(async move |cx| {
102            let input = input
103                .recv()
104                .await
105                .map_err(|e| SpawnAgentToolOutput::Error {
106                    session_id: None,
107                    error: format!("Failed to receive tool input: {e}"),
108                })?;
109
110            let (subagent, subagent_session_id) = cx.update(|cx| {
111                let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
112                    return Err(SpawnAgentToolOutput::Error {
113                        session_id: None,
114                        error: "Parent thread no longer exists".to_string(),
115                    });
116                };
117
118                let subagent = if let Some(session_id) = input.session_id {
119                    self.environment.resume_subagent(
120                        parent_thread_entity,
121                        session_id,
122                        input.message,
123                        cx,
124                    )
125                } else {
126                    self.environment.create_subagent(
127                        parent_thread_entity,
128                        input.label,
129                        input.message,
130                        cx,
131                    )
132                };
133                let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
134                    session_id: None,
135                    error: err.to_string(),
136                })?;
137                let subagent_session_id = subagent.id();
138
139                event_stream.subagent_spawned(subagent_session_id.clone());
140                let meta = acp::Meta::from_iter([(
141                    SUBAGENT_SESSION_ID_META_KEY.into(),
142                    subagent_session_id.to_string().into(),
143                )]);
144                event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
145
146                Ok((subagent, subagent_session_id))
147            })?;
148
149            match subagent.wait_for_output(cx).await {
150                Ok(output) => {
151                    event_stream.update_fields(
152                        acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
153                    );
154                    Ok(SpawnAgentToolOutput::Success {
155                        session_id: subagent_session_id,
156                        output,
157                    })
158                }
159                Err(e) => {
160                    let error = e.to_string();
161                    event_stream.update_fields(
162                        acp::ToolCallUpdateFields::new().content(vec![error.clone().into()]),
163                    );
164                    Err(SpawnAgentToolOutput::Error {
165                        session_id: Some(subagent_session_id),
166                        error,
167                    })
168                }
169            }
170        })
171    }
172
173    fn replay(
174        &self,
175        _input: Self::Input,
176        output: Self::Output,
177        event_stream: ToolCallEventStream,
178        _cx: &mut App,
179    ) -> Result<()> {
180        let session_id = match &output {
181            SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id),
182            SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(),
183        };
184
185        if let Some(session_id) = session_id {
186            event_stream.subagent_spawned(session_id.clone());
187            let meta = acp::Meta::from_iter([(
188                SUBAGENT_SESSION_ID_META_KEY.into(),
189                session_id.to_string().into(),
190            )]);
191            event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
192        }
193
194        let content = match &output {
195            SpawnAgentToolOutput::Success { output, .. } => output.into(),
196            SpawnAgentToolOutput::Error { error, .. } => error.into(),
197        };
198        event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![content]));
199
200        Ok(())
201    }
202}