Does not actually seem useful to the LLM to include `Failed to receive tool input: ...` in the error message. We now only include the actual error. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A
257 lines
9.6 KiB
Rust
257 lines
9.6 KiB
Rust
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(())
|
|
}
|
|
}
|