256 lines · 9.6 KB · Rust
RawDownload
use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo};use agent_client_protocol::schema 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}; /// Spawn a sub-agent for a well-scoped task.////// ### Designing delegated subtasks/// - An agent does not see your conversation history. Include all relevant context (file paths, requirements, constraints) in the message./// - Subtasks must be concrete, well-defined, and self-contained./// - Delegated subtasks must materially advance the main task./// - Do not duplicate work between your work and delegated subtasks./// - Do not use this tool for tasks you could accomplish directly with one or two tool calls./// - When you delegate work, focus on coordinating and synthesizing results instead of duplicating the same work yourself./// - Avoid issuing multiple delegate calls for the same unresolved subproblem unless the new delegated task is genuinely different and necessary./// - Narrow the delegated ask to the concrete output you need next./// - For code-edit subtasks, decompose work so each delegated task has a disjoint write set./// - When sending a follow-up using an existing agent session_id, the agent already has the context from the previous turn. Send only a short, direct message. Do NOT repeat the original task or context.////// ### Parallel delegation patterns/// - Run multiple independent information-seeking subtasks in parallel when you have distinct questions that can be answered independently./// - Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap./// - When a plan has multiple independent steps, prefer delegating those steps in parallel rather than serializing them unnecessarily./// - Reuse the returned session_id when you want to follow up on the same delegated subproblem instead of creating a duplicate session.////// ### Output/// - You will receive only the agent's final message as output./// - Successful calls return a session_id that you can use for follow-up messages./// - Error results may also include a session_id if a session was already created.#[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: e.to_string(),                    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(())    }}