use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo};use agent_client_protocol 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}; /// Spawns an agent to perform a delegated task.////// Use this tool when you want to:/// - Run multiple tasks in parallel./// - Delegate a self-contained task where you only need the final outcome.////// Do NOT use this tool for tasks you could accomplish directly with one or two tool calls (e.g. reading a file, running a single command).////// You will receive only the agent's final message as output.////// **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.////// **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".////// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).#[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<acp::SessionId>,} #[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<acp::SessionId>, error: String, session_info: Option<SubagentSessionInfo>, },} impl From<SpawnAgentToolOutput> 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<dyn ThreadEnvironment>,} impl SpawnAgentTool { pub fn new(environment: Rc<dyn ThreadEnvironment>) -> 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<Self::Input, serde_json::Value>, _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<Self>, input: ToolInput<Self::Input>, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<Self::Output, Self::Output>> { cx.spawn(async move |cx| { let input = input .recv() .await .map_err(|e| SpawnAgentToolOutput::Error { session_id: None, error: format!("Failed to receive tool input: {e}"), 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(()) }}
use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo};
use agent_client_protocol 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};
/// Spawns an agent to perform a delegated task.
///
/// Use this tool when you want to:
/// - Run multiple tasks in parallel.
/// - Delegate a self-contained task where you only need the final outcome.
///
/// Do NOT use this tool for tasks you could accomplish directly with one or two tool calls (e.g. reading a file, running a single command).
///
/// You will receive only the agent's final message as output.
///
/// **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.
///
/// **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".
///
/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).
#[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<acp::SessionId>,
}
#[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<acp::SessionId>,
error: String,
session_info: Option<SubagentSessionInfo>,
},
}
impl From<SpawnAgentToolOutput> 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<dyn ThreadEnvironment>,
}
impl SpawnAgentTool {
pub fn new(environment: Rc<dyn ThreadEnvironment>) -> 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<Self::Input, serde_json::Value>,
_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<Self>,
input: ToolInput<Self::Input>,
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output, Self::Output>> {
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| SpawnAgentToolOutput::Error {
session_id: None,
error: format!("Failed to receive tool input: {e}"),
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(())
}
}