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/// Spawn a sub-agent for a well-scoped task.
 14///
 15/// ### Designing delegated subtasks
 16/// - An agent does not see your conversation history. Include all relevant context (file paths, requirements, constraints) in the message.
 17/// - Subtasks must be concrete, well-defined, and self-contained.
 18/// - Delegated subtasks must materially advance the main task.
 19/// - Do not duplicate work between your work and delegated subtasks.
 20/// - Do not use this tool for tasks you could accomplish directly with one or two tool calls.
 21/// - When you delegate work, focus on coordinating and synthesizing results instead of duplicating the same work yourself.
 22/// - Avoid issuing multiple delegate calls for the same unresolved subproblem unless the new delegated task is genuinely different and necessary.
 23/// - Narrow the delegated ask to the concrete output you need next.
 24/// - For code-edit subtasks, decompose work so each delegated task has a disjoint write set.
 25/// - When sending a follow-up using an existing agent session_id, the agent already has the context from the previous turn. Send only a short, direct message. Do NOT repeat the original task or context.
 26///
 27/// ### Parallel delegation patterns
 28/// - Run multiple independent information-seeking subtasks in parallel when you have distinct questions that can be answered independently.
 29/// - Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap.
 30/// - When a plan has multiple independent steps, prefer delegating those steps in parallel rather than serializing them unnecessarily.
 31/// - Reuse the returned session_id when you want to follow up on the same delegated subproblem instead of creating a duplicate session.
 32///
 33/// ### Output
 34/// - You will receive only the agent's final message as output.
 35/// - Successful calls return a session_id that you can use for follow-up messages.
 36/// - Error results may also include a session_id if a session was already created.
 37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 38#[serde(rename_all = "snake_case")]
 39pub struct SpawnAgentToolInput {
 40    /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
 41    pub label: String,
 42    /// 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.
 43    pub message: String,
 44    /// Session ID of an existing agent session to continue instead of creating a new one.
 45    #[serde(default)]
 46    pub session_id: Option<acp::SessionId>,
 47}
 48
 49#[derive(Debug, Clone, Serialize, Deserialize)]
 50#[serde(untagged)]
 51#[serde(rename_all = "snake_case")]
 52pub enum SpawnAgentToolOutput {
 53    Success {
 54        session_id: acp::SessionId,
 55        output: String,
 56        session_info: SubagentSessionInfo,
 57    },
 58    Error {
 59        #[serde(skip_serializing_if = "Option::is_none")]
 60        #[serde(default)]
 61        session_id: Option<acp::SessionId>,
 62        error: String,
 63        session_info: Option<SubagentSessionInfo>,
 64    },
 65}
 66
 67impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
 68    fn from(output: SpawnAgentToolOutput) -> Self {
 69        match output {
 70            SpawnAgentToolOutput::Success {
 71                session_id,
 72                output,
 73                session_info: _, // Don't show this to the model
 74            } => serde_json::to_string(
 75                &serde_json::json!({ "session_id": session_id, "output": output }),
 76            )
 77            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
 78            .into(),
 79            SpawnAgentToolOutput::Error {
 80                session_id,
 81                error,
 82                session_info: _, // Don't show this to the model
 83            } => serde_json::to_string(
 84                &serde_json::json!({ "session_id": session_id, "error": error }),
 85            )
 86            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
 87            .into(),
 88        }
 89    }
 90}
 91
 92/// Tool that spawns an agent thread to work on a task.
 93pub struct SpawnAgentTool {
 94    environment: Rc<dyn ThreadEnvironment>,
 95}
 96
 97impl SpawnAgentTool {
 98    pub fn new(environment: Rc<dyn ThreadEnvironment>) -> Self {
 99        Self { environment }
100    }
101}
102
103impl AgentTool for SpawnAgentTool {
104    type Input = SpawnAgentToolInput;
105    type Output = SpawnAgentToolOutput;
106
107    const NAME: &'static str = "spawn_agent";
108
109    fn kind() -> acp::ToolKind {
110        acp::ToolKind::Other
111    }
112
113    fn initial_title(
114        &self,
115        input: Result<Self::Input, serde_json::Value>,
116        _cx: &mut App,
117    ) -> SharedString {
118        match input {
119            Ok(i) => i.label.into(),
120            Err(value) => value
121                .get("label")
122                .and_then(|v| v.as_str())
123                .map(|s| SharedString::from(s.to_owned()))
124                .unwrap_or_else(|| "Spawning agent".into()),
125        }
126    }
127
128    fn run(
129        self: Arc<Self>,
130        input: ToolInput<Self::Input>,
131        event_stream: ToolCallEventStream,
132        cx: &mut App,
133    ) -> Task<Result<Self::Output, Self::Output>> {
134        cx.spawn(async move |cx| {
135            let input = input
136                .recv()
137                .await
138                .map_err(|e| SpawnAgentToolOutput::Error {
139                    session_id: None,
140                    error: format!("Failed to receive tool input: {e}"),
141                    session_info: None,
142                })?;
143
144            let (subagent, mut session_info) = cx.update(|cx| {
145                let subagent = if let Some(session_id) = input.session_id {
146                    self.environment.resume_subagent(session_id, cx)
147                } else {
148                    self.environment.create_subagent(input.label, cx)
149                };
150                let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
151                    session_id: None,
152                    error: err.to_string(),
153                    session_info: None,
154                })?;
155                let session_info = SubagentSessionInfo {
156                    session_id: subagent.id(),
157                    message_start_index: subagent.num_entries(cx),
158                    message_end_index: None,
159                };
160
161                event_stream.subagent_spawned(subagent.id());
162                event_stream.update_fields_with_meta(
163                    acp::ToolCallUpdateFields::new(),
164                    Some(acp::Meta::from_iter([(
165                        SUBAGENT_SESSION_INFO_META_KEY.into(),
166                        serde_json::json!(&session_info),
167                    )])),
168                );
169
170                Ok((subagent, session_info))
171            })?;
172
173            let send_result = subagent.send(input.message, cx).await;
174
175            let status = if send_result.is_ok() {
176                "completed"
177            } else {
178                "error"
179            };
180            telemetry::event!(
181                "Subagent Completed",
182                subagent_session = session_info.session_id.to_string(),
183                status,
184            );
185
186            session_info.message_end_index =
187                cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1)));
188
189            let meta = Some(acp::Meta::from_iter([(
190                SUBAGENT_SESSION_INFO_META_KEY.into(),
191                serde_json::json!(&session_info),
192            )]));
193
194            let (output, result) = match send_result {
195                Ok(output) => (
196                    output.clone(),
197                    Ok(SpawnAgentToolOutput::Success {
198                        session_id: session_info.session_id.clone(),
199                        session_info,
200                        output,
201                    }),
202                ),
203                Err(e) => {
204                    let error = e.to_string();
205                    (
206                        error.clone(),
207                        Err(SpawnAgentToolOutput::Error {
208                            session_id: Some(session_info.session_id.clone()),
209                            error,
210                            session_info: Some(session_info),
211                        }),
212                    )
213                }
214            };
215            event_stream.update_fields_with_meta(
216                acp::ToolCallUpdateFields::new().content(vec![output.into()]),
217                meta,
218            );
219            result
220        })
221    }
222
223    fn replay(
224        &self,
225        _input: Self::Input,
226        output: Self::Output,
227        event_stream: ToolCallEventStream,
228        _cx: &mut App,
229    ) -> Result<()> {
230        let (content, session_info) = match output {
231            SpawnAgentToolOutput::Success {
232                output,
233                session_info,
234                ..
235            } => (output.into(), Some(session_info)),
236            SpawnAgentToolOutput::Error {
237                error,
238                session_info,
239                ..
240            } => (error.into(), session_info),
241        };
242
243        let meta = session_info.map(|session_info| {
244            acp::Meta::from_iter([(
245                SUBAGENT_SESSION_INFO_META_KEY.into(),
246                serde_json::json!(&session_info),
247            )])
248        });
249        event_stream.update_fields_with_meta(
250            acp::ToolCallUpdateFields::new().content(vec![content]),
251            meta,
252        );
253
254        Ok(())
255    }
256}