247 lines · 8.7 KB · Rust
RawDownload
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(())    }}