spawn_subagent_tool.rs

  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 _;