1use crate::{AgentTool, ToolCallEventStream};
2use agent_client_protocol as acp;
3use anyhow::{Result, anyhow};
4use gpui::{App, AsyncApp, SharedString, Task};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9/// Spawn a subagent (child thread) that can be visited while it runs, and returns a value to the parent.
10///
11/// Note: This file intentionally defines only the tool surface and streaming updates. The actual
12/// spawning/navigation plumbing requires a host capability (session manager + UI) that is not yet
13/// present in the native agent tool environment. Until that capability is wired in, this tool will
14/// fail with a clear error.
15///
16/// Expected design (to be implemented in the host):
17/// - The tool is constructed with a `SubagentHost` implementation that can:
18/// - create a child session/thread
19/// - stream child progress updates
20/// - complete with a final return value
21/// - provide a navigable URI for the UI (e.g. `zed://agent/thread/<session_id>`)
22///
23/// The tool then:
24/// - emits a `ResourceLink` pointing at the child thread so users can open it
25/// - streams progress into the tool call card as markdown
26/// - resolves with the child's final return value (string)
27#[derive(JsonSchema, Serialize, Deserialize)]
28pub struct SpawnSubagentToolInput {
29 /// A short label/title for the subagent.
30 pub title: String,
31
32 /// The instructions to run in the subagent.
33 pub prompt: String,
34
35 /// Optional: profile id to use for the subagent.
36 #[serde(default)]
37 pub profile_id: Option<String>,
38}
39
40/// The final return value from the subagent.
41pub type SpawnSubagentToolOutput = String;
42
43/// Host interface required to implement spawning + streaming + returning.
44///
45/// This is intentionally minimal and object-safe to allow injecting a host backed by `NativeAgent`.
46pub trait SubagentHost: Send + Sync + 'static {
47 /// Start a child subagent session and return a handle containing a navigable URI plus a stream
48 /// of progress updates and a final result.
49 ///
50 /// The returned `SubagentRun` must:
51 /// - yield `Progress` updates in-order
52 /// - eventually yield exactly one `Final` or `Error`
53 fn spawn_subagent(
54 &self,
55 title: String,
56 prompt: String,
57 profile_id: Option<String>,
58 cx: &mut AsyncApp,
59 ) -> Task<Result<SubagentRun>>;
60}
61
62/// A handle for a running subagent.
63pub struct SubagentRun {
64 /// URI that the UI can open to navigate to the child thread.
65 pub thread_uri: String,
66
67 /// A human-friendly label for the link.
68 pub thread_label: String,
69
70 /// Progress stream for tool UI updates.
71 pub updates: futures::channel::mpsc::UnboundedReceiver<SubagentUpdate>,
72}
73
74pub enum SubagentUpdate {
75 /// A streaming progress chunk (e.g. "thinking…", partial summary, etc).
76 Progress(String),
77
78 /// The final return value for the parent.
79 Final(String),
80
81 /// Terminal error.
82 Error(anyhow::Error),
83}
84
85pub struct SpawnSubagentTool {
86 host: Option<Arc<dyn SubagentHost>>,
87}
88
89impl SpawnSubagentTool {
90 pub fn new(host: Option<Arc<dyn SubagentHost>>) -> Self {
91 Self { host }
92 }
93}
94
95impl AgentTool for SpawnSubagentTool {
96 type Input = SpawnSubagentToolInput;
97 type Output = SpawnSubagentToolOutput;
98
99 fn name() -> &'static str {
100 "spawn_subagent"
101 }
102
103 fn kind() -> acp::ToolKind {
104 acp::ToolKind::Other
105 }
106
107 fn description() -> SharedString {
108 "Spawns a child Zed Agent thread (subagent), streams its progress, and returns its final value to the parent."
109 .into()
110 }
111
112 fn initial_title(
113 &self,
114 input: Result<Self::Input, serde_json::Value>,
115 _cx: &mut App,
116 ) -> SharedString {
117 if let Ok(input) = input {
118 format!("Spawn subagent: {}", input.title).into()
119 } else {
120 "Spawn subagent".into()
121 }
122 }
123
124 fn run(
125 self: Arc<Self>,
126 input: Self::Input,
127 event_stream: ToolCallEventStream,
128 cx: &mut App,
129 ) -> Task<Result<Self::Output>> {
130 let Some(host) = self.host.clone() else {
131 return Task::ready(Err(anyhow!(
132 "spawn_subagent is not available: native agent host capability is not wired into tools yet"
133 )));
134 };
135
136 let title = input.title;
137 let prompt = input.prompt;
138 let profile_id = input.profile_id;
139
140 cx.spawn(async move |cx| {
141 // Start the child run via host.
142 let mut run = host
143 .spawn_subagent(title.clone(), prompt, profile_id, cx)
144 .await?;
145
146 // Emit a link to the child thread so the user can open/visit it.
147 event_stream.update_fields(
148 acp::ToolCallUpdateFields::new().content(vec![acp::ToolCallContent::Content(
149 acp::Content::new(acp::ContentBlock::ResourceLink(
150 acp::ResourceLink::new(run.thread_label.clone(), run.thread_uri.clone())
151 .title(run.thread_label.clone()),
152 )),
153 )]),
154 );
155
156 // Stream progress as markdown appended below the link.
157 let mut accumulated_progress = String::new();
158 while let Some(update) = run.updates.next().await {
159 match update {
160 SubagentUpdate::Progress(chunk) => {
161 if !accumulated_progress.is_empty() {
162 accumulated_progress.push('\n');
163 }
164 accumulated_progress.push_str(&chunk);
165
166 event_stream.update_fields(
167 acp::ToolCallUpdateFields::new().content(vec![
168 acp::ToolCallContent::Content(acp::Content::new(
169 acp::ContentBlock::ResourceLink(
170 acp::ResourceLink::new(
171 run.thread_label.clone(),
172 run.thread_uri.clone(),
173 )
174 .title(run.thread_label.clone()),
175 ),
176 )),
177 acp::ToolCallContent::Content(acp::Content::new(
178 acp::ContentBlock::Text(acp::TextContent::new(
179 format!("### Subagent progress\n\n{}", accumulated_progress),
180 )),
181 )),
182 ]),
183 );
184 }
185 SubagentUpdate::Final(value) => {
186 // Final update for UI (optional).
187 event_stream.update_fields(
188 acp::ToolCallUpdateFields::new().content(vec![
189 acp::ToolCallContent::Content(acp::Content::new(
190 acp::ContentBlock::ResourceLink(
191 acp::ResourceLink::new(
192 run.thread_label.clone(),
193 run.thread_uri.clone(),
194 )
195 .title(run.thread_label.clone()),
196 ),
197 )),
198 acp::ToolCallContent::Content(acp::Content::new(
199 acp::ContentBlock::Text(acp::TextContent::new(format!(
200 "### Subagent returned\n\n{}",
201 value
202 ))),
203 )),
204 ]),
205 );
206
207 return Ok(value);
208 }
209 SubagentUpdate::Error(error) => {
210 return Err(error);
211 }
212 }
213 }
214
215 Err(anyhow!("subagent stream ended without producing a final value"))
216 })
217 }
218}
219
220// futures::StreamExt is only needed in the async run implementation; keep it scoped here.
221use futures::StreamExt as _;