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