use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo}; use agent_client_protocol::schema as acp; use anyhow::Result; use gpui::{App, SharedString, Task}; use language_model::LanguageModelToolResultContent; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::rc::Rc; use std::sync::Arc; use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream, ToolInput}; /// Spawn a sub-agent for a well-scoped task. /// /// ### Designing delegated subtasks /// - An agent does not see your conversation history. Include all relevant context (file paths, requirements, constraints) in the message. /// - Subtasks must be concrete, well-defined, and self-contained. /// - Delegated subtasks must materially advance the main task. /// - Do not duplicate work between your work and delegated subtasks. /// - Do not use this tool for tasks you could accomplish directly with one or two tool calls. /// - When you delegate work, focus on coordinating and synthesizing results instead of duplicating the same work yourself. /// - Avoid issuing multiple delegate calls for the same unresolved subproblem unless the new delegated task is genuinely different and necessary. /// - Narrow the delegated ask to the concrete output you need next. /// - For code-edit subtasks, decompose work so each delegated task has a disjoint write set. /// - 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. /// /// ### Parallel delegation patterns /// - Run multiple independent information-seeking subtasks in parallel when you have distinct questions that can be answered independently. /// - Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap. /// - When a plan has multiple independent steps, prefer delegating those steps in parallel rather than serializing them unnecessarily. /// - Reuse the returned session_id when you want to follow up on the same delegated subproblem instead of creating a duplicate session. /// /// ### Output /// - You will receive only the agent's final message as output. /// - Successful calls return a session_id that you can use for follow-up messages. /// - Error results may also include a session_id if a session was already created. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct SpawnAgentToolInput { /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives") pub label: String, /// 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. pub message: String, /// Session ID of an existing agent session to continue instead of creating a new one. #[serde(default)] pub session_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] #[serde(rename_all = "snake_case")] pub enum SpawnAgentToolOutput { Success { session_id: acp::SessionId, output: String, session_info: SubagentSessionInfo, }, Error { #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] session_id: Option, error: String, session_info: Option, }, } impl From for LanguageModelToolResultContent { fn from(output: SpawnAgentToolOutput) -> Self { match output { SpawnAgentToolOutput::Success { session_id, output, session_info: _, // Don't show this to the model } => serde_json::to_string( &serde_json::json!({ "session_id": session_id, "output": output }), ) .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}")) .into(), SpawnAgentToolOutput::Error { session_id, error, session_info: _, // Don't show this to the model } => serde_json::to_string( &serde_json::json!({ "session_id": session_id, "error": error }), ) .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}")) .into(), } } } /// Tool that spawns an agent thread to work on a task. pub struct SpawnAgentTool { environment: Rc, } impl SpawnAgentTool { pub fn new(environment: Rc) -> Self { Self { environment } } } impl AgentTool for SpawnAgentTool { type Input = SpawnAgentToolInput; type Output = SpawnAgentToolOutput; const NAME: &'static str = "spawn_agent"; fn kind() -> acp::ToolKind { acp::ToolKind::Other } fn initial_title( &self, input: Result, _cx: &mut App, ) -> SharedString { match input { Ok(i) => i.label.into(), Err(value) => value .get("label") .and_then(|v| v.as_str()) .map(|s| SharedString::from(s.to_owned())) .unwrap_or_else(|| "Spawning agent".into()), } } fn run( self: Arc, input: ToolInput, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { cx.spawn(async move |cx| { let input = input .recv() .await .map_err(|e| SpawnAgentToolOutput::Error { session_id: None, error: e.to_string(), session_info: None, })?; let (subagent, mut session_info) = cx.update(|cx| { let subagent = if let Some(session_id) = input.session_id { self.environment.resume_subagent(session_id, cx) } else { self.environment.create_subagent(input.label, cx) }; let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error { session_id: None, error: err.to_string(), session_info: None, })?; let session_info = SubagentSessionInfo { session_id: subagent.id(), message_start_index: subagent.num_entries(cx), message_end_index: None, }; event_stream.subagent_spawned(subagent.id()); event_stream.update_fields_with_meta( acp::ToolCallUpdateFields::new(), Some(acp::Meta::from_iter([( SUBAGENT_SESSION_INFO_META_KEY.into(), serde_json::json!(&session_info), )])), ); Ok((subagent, session_info)) })?; let send_result = subagent.send(input.message, cx).await; let status = if send_result.is_ok() { "completed" } else { "error" }; telemetry::event!( "Subagent Completed", subagent_session = session_info.session_id.to_string(), status, ); session_info.message_end_index = cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1))); let meta = Some(acp::Meta::from_iter([( SUBAGENT_SESSION_INFO_META_KEY.into(), serde_json::json!(&session_info), )])); let (output, result) = match send_result { Ok(output) => ( output.clone(), Ok(SpawnAgentToolOutput::Success { session_id: session_info.session_id.clone(), session_info, output, }), ), Err(e) => { let error = e.to_string(); ( error.clone(), Err(SpawnAgentToolOutput::Error { session_id: Some(session_info.session_id.clone()), error, session_info: Some(session_info), }), ) } }; event_stream.update_fields_with_meta( acp::ToolCallUpdateFields::new().content(vec![output.into()]), meta, ); result }) } fn replay( &self, _input: Self::Input, output: Self::Output, event_stream: ToolCallEventStream, _cx: &mut App, ) -> Result<()> { let (content, session_info) = match output { SpawnAgentToolOutput::Success { output, session_info, .. } => (output.into(), Some(session_info)), SpawnAgentToolOutput::Error { error, session_info, .. } => (error.into(), session_info), }; let meta = session_info.map(|session_info| { acp::Meta::from_iter([( SUBAGENT_SESSION_INFO_META_KEY.into(), serde_json::json!(&session_info), )]) }); event_stream.update_fields_with_meta( acp::ToolCallUpdateFields::new().content(vec![content]), meta, ); Ok(()) } }