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};
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/// 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.
21///
22/// You will receive only the agent's final message as output.
23///
24/// 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.
25///
26/// Note:
27/// - Agents cannot use tools you don't have access to.
28/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
30pub struct SpawnAgentToolInput {
31 /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
32 pub label: String,
33 /// 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.
34 pub message: String,
35 /// 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.
36 #[serde(default)]
37 pub session_id: Option<acp::SessionId>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
41#[serde(untagged)]
42pub enum SpawnAgentToolOutput {
43 Success {
44 session_id: acp::SessionId,
45 output: String,
46 },
47 Error {
48 #[serde(skip_serializing_if = "Option::is_none")]
49 #[serde(default)]
50 session_id: Option<acp::SessionId>,
51 error: String,
52 },
53}
54
55impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
56 fn from(output: SpawnAgentToolOutput) -> Self {
57 serde_json::to_string(&output)
58 .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
59 .into()
60 }
61}
62
63/// Tool that spawns an agent thread to work on a task.
64pub struct SpawnAgentTool {
65 parent_thread: WeakEntity<Thread>,
66 environment: Rc<dyn ThreadEnvironment>,
67}
68
69impl SpawnAgentTool {
70 pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
71 Self {
72 parent_thread,
73 environment,
74 }
75 }
76}
77
78impl AgentTool for SpawnAgentTool {
79 type Input = SpawnAgentToolInput;
80 type Output = SpawnAgentToolOutput;
81
82 const NAME: &'static str = "spawn_agent";
83
84 fn kind() -> acp::ToolKind {
85 acp::ToolKind::Other
86 }
87
88 fn initial_title(
89 &self,
90 input: Result<Self::Input, serde_json::Value>,
91 _cx: &mut App,
92 ) -> SharedString {
93 input
94 .map(|i| i.label.into())
95 .unwrap_or_else(|_| "Spawning agent".into())
96 }
97
98 fn run(
99 self: Arc<Self>,
100 input: Self::Input,
101 event_stream: ToolCallEventStream,
102 cx: &mut App,
103 ) -> Task<Result<Self::Output, Self::Output>> {
104 let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
105 return Task::ready(Err(SpawnAgentToolOutput::Error {
106 session_id: None,
107 error: "Parent thread no longer exists".to_string(),
108 }));
109 };
110
111 let subagent = if let Some(session_id) = input.session_id {
112 self.environment
113 .resume_subagent(parent_thread_entity, session_id, input.message, cx)
114 } else {
115 self.environment
116 .create_subagent(parent_thread_entity, input.label, input.message, cx)
117 };
118 let subagent = match subagent {
119 Ok(subagent) => subagent,
120 Err(err) => {
121 return Task::ready(Err(SpawnAgentToolOutput::Error {
122 session_id: None,
123 error: err.to_string(),
124 }));
125 }
126 };
127 let subagent_session_id = subagent.id();
128
129 event_stream.subagent_spawned(subagent_session_id.clone());
130 let meta = acp::Meta::from_iter([(
131 SUBAGENT_SESSION_ID_META_KEY.into(),
132 subagent_session_id.to_string().into(),
133 )]);
134 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
135
136 cx.spawn(async move |cx| match subagent.wait_for_output(cx).await {
137 Ok(output) => {
138 event_stream.update_fields(
139 acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
140 );
141 Ok(SpawnAgentToolOutput::Success {
142 session_id: subagent_session_id,
143 output,
144 })
145 }
146 Err(e) => {
147 let error = e.to_string();
148 event_stream.update_fields(
149 acp::ToolCallUpdateFields::new().content(vec![error.clone().into()]),
150 );
151 Err(SpawnAgentToolOutput::Error {
152 session_id: Some(subagent_session_id),
153 error,
154 })
155 }
156 })
157 }
158
159 fn replay(
160 &self,
161 _input: Self::Input,
162 output: Self::Output,
163 event_stream: ToolCallEventStream,
164 _cx: &mut App,
165 ) -> Result<()> {
166 let session_id = match &output {
167 SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id),
168 SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(),
169 };
170
171 if let Some(session_id) = session_id {
172 event_stream.subagent_spawned(session_id.clone());
173 let meta = acp::Meta::from_iter([(
174 SUBAGENT_SESSION_ID_META_KEY.into(),
175 session_id.to_string().into(),
176 )]);
177 event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
178 }
179
180 let content = match &output {
181 SpawnAgentToolOutput::Success { output, .. } => output.into(),
182 SpawnAgentToolOutput::Error { error, .. } => error.into(),
183 };
184 event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![content]));
185
186 Ok(())
187 }
188}