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/// 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)]
 27#[serde(rename_all = "snake_case")]
 28pub struct SpawnAgentToolInput {
 29    /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
 30    pub label: String,
 31    /// 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.
 32    pub message: String,
 33    /// Session ID of an existing agent session to continue instead of creating a new one.
 34    #[serde(default)]
 35    pub session_id: Option<acp::SessionId>,
 36}
 37
 38#[derive(Debug, Clone, Serialize, Deserialize)]
 39#[serde(untagged)]
 40#[serde(rename_all = "snake_case")]
 41pub enum SpawnAgentToolOutput {
 42    Success {
 43        session_id: acp::SessionId,
 44        output: String,
 45        session_info: SubagentSessionInfo,
 46    },
 47    Error {
 48        #[serde(skip_serializing_if = "Option::is_none")]
 49        #[serde(default)]
 50        session_id: Option<acp::SessionId>,
 51        error: String,
 52        session_info: Option<SubagentSessionInfo>,
 53    },
 54}
 55
 56impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
 57    fn from(output: SpawnAgentToolOutput) -> Self {
 58        match output {
 59            SpawnAgentToolOutput::Success {
 60                session_id,
 61                output,
 62                session_info: _, // Don't show this to the model
 63            } => serde_json::to_string(
 64                &serde_json::json!({ "session_id": session_id, "output": output }),
 65            )
 66            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
 67            .into(),
 68            SpawnAgentToolOutput::Error {
 69                session_id,
 70                error,
 71                session_info: _, // Don't show this to the model
 72            } => serde_json::to_string(
 73                &serde_json::json!({ "session_id": session_id, "error": error }),
 74            )
 75            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
 76            .into(),
 77        }
 78    }
 79}
 80
 81/// Tool that spawns an agent thread to work on a task.
 82pub struct SpawnAgentTool {
 83    environment: Rc<dyn ThreadEnvironment>,
 84}
 85
 86impl SpawnAgentTool {
 87    pub fn new(environment: Rc<dyn ThreadEnvironment>) -> Self {
 88        Self { environment }
 89    }
 90}
 91
 92impl AgentTool for SpawnAgentTool {
 93    type Input = SpawnAgentToolInput;
 94    type Output = SpawnAgentToolOutput;
 95
 96    const NAME: &'static str = "spawn_agent";
 97
 98    fn kind() -> acp::ToolKind {
 99        acp::ToolKind::Other
100    }
101
102    fn initial_title(
103        &self,
104        input: Result<Self::Input, serde_json::Value>,
105        _cx: &mut App,
106    ) -> SharedString {
107        match input {
108            Ok(i) => i.label.into(),
109            Err(value) => value
110                .get("label")
111                .and_then(|v| v.as_str())
112                .map(|s| SharedString::from(s.to_owned()))
113                .unwrap_or_else(|| "Spawning agent".into()),
114        }
115    }
116
117    fn run(
118        self: Arc<Self>,
119        input: ToolInput<Self::Input>,
120        event_stream: ToolCallEventStream,
121        cx: &mut App,
122    ) -> Task<Result<Self::Output, Self::Output>> {
123        cx.spawn(async move |cx| {
124            let input = input
125                .recv()
126                .await
127                .map_err(|e| SpawnAgentToolOutput::Error {
128                    session_id: None,
129                    error: format!("Failed to receive tool input: {e}"),
130                    session_info: None,
131                })?;
132
133            let (subagent, mut session_info) = cx.update(|cx| {
134                let subagent = if let Some(session_id) = input.session_id {
135                    self.environment.resume_subagent(session_id, cx)
136                } else {
137                    self.environment.create_subagent(input.label, cx)
138                };
139                let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
140                    session_id: None,
141                    error: err.to_string(),
142                    session_info: None,
143                })?;
144                let session_info = SubagentSessionInfo {
145                    session_id: subagent.id(),
146                    message_start_index: subagent.num_entries(cx),
147                    message_end_index: None,
148                };
149
150                event_stream.subagent_spawned(subagent.id());
151                event_stream.update_fields_with_meta(
152                    acp::ToolCallUpdateFields::new(),
153                    Some(acp::Meta::from_iter([(
154                        SUBAGENT_SESSION_INFO_META_KEY.into(),
155                        serde_json::json!(&session_info),
156                    )])),
157                );
158
159                Ok((subagent, session_info))
160            })?;
161
162            match subagent.send(input.message, cx).await {
163                Ok(output) => {
164                    session_info.message_end_index =
165                        cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1)));
166                    event_stream.update_fields_with_meta(
167                        acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
168                        Some(acp::Meta::from_iter([(
169                            SUBAGENT_SESSION_INFO_META_KEY.into(),
170                            serde_json::json!(&session_info),
171                        )])),
172                    );
173                    Ok(SpawnAgentToolOutput::Success {
174                        session_id: session_info.session_id.clone(),
175                        session_info,
176                        output,
177                    })
178                }
179                Err(e) => Err(SpawnAgentToolOutput::Error {
180                    session_id: Some(session_info.session_id.clone()),
181                    error: e.to_string(),
182                    session_info: Some(session_info),
183                }),
184            }
185        })
186    }
187
188    fn replay(
189        &self,
190        _input: Self::Input,
191        output: Self::Output,
192        event_stream: ToolCallEventStream,
193        _cx: &mut App,
194    ) -> Result<()> {
195        let (content, session_info) = match output {
196            SpawnAgentToolOutput::Success {
197                output,
198                session_info,
199                ..
200            } => (output.into(), Some(session_info)),
201            SpawnAgentToolOutput::Error {
202                error,
203                session_info,
204                ..
205            } => (error.into(), session_info),
206        };
207
208        let meta = session_info.map(|session_info| {
209            acp::Meta::from_iter([(
210                SUBAGENT_SESSION_INFO_META_KEY.into(),
211                serde_json::json!(&session_info),
212            )])
213        });
214        event_stream.update_fields_with_meta(
215            acp::ToolCallUpdateFields::new().content(vec![content]),
216            meta,
217        );
218
219        Ok(())
220    }
221}