Removes tool filtering since this was throwing off certain models, and also allows for more generic task prompts that don't always require summaries. Since the models usually provide a wrap-up message, we don't have to wait for another turn. This also sets us up to allow the agent to re-interact with an existing subagent thread. Release Notes: - N/A --------- Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
142 lines
4.7 KiB
Rust
142 lines
4.7 KiB
Rust
use acp_thread::SUBAGENT_SESSION_ID_META_KEY;
|
|
use agent_client_protocol as acp;
|
|
use anyhow::{Result, anyhow};
|
|
use gpui::{App, SharedString, Task, WeakEntity};
|
|
use language_model::LanguageModelToolResultContent;
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
use std::{rc::Rc, time::Duration};
|
|
|
|
use crate::{AgentTool, Thread, ThreadEnvironment, ToolCallEventStream};
|
|
|
|
/// Spawns an agent to perform a delegated task.
|
|
///
|
|
/// Use this tool when you want to do any of the following:
|
|
/// - Perform an investigation where all you need to know is the outcome, not the research that led to that outcome.
|
|
/// - Complete a self-contained task where you need to know if it succeeded or failed (and how), but none of its intermediate output.
|
|
/// - Run multiple tasks in parallel that would take significantly longer to run sequentially.
|
|
///
|
|
/// You control what the agent does by providing a prompt describing what the agent should do. The agent has access to the same tools you do.
|
|
///
|
|
/// You will receive the agent's final message.
|
|
///
|
|
/// Note:
|
|
/// - Agents cannot use tools you don't have access to.
|
|
/// - 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)]
|
|
pub struct SubagentToolInput {
|
|
/// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
|
|
pub label: String,
|
|
/// The prompt that tells the agent what task to perform. Be specific about what you want the agent to accomplish.
|
|
pub prompt: String,
|
|
/// Optional: Maximum runtime in seconds. No timeout by default.
|
|
#[serde(default)]
|
|
pub timeout: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
|
pub struct SubagentToolOutput {
|
|
pub session_id: acp::SessionId,
|
|
pub output: String,
|
|
}
|
|
|
|
impl From<SubagentToolOutput> for LanguageModelToolResultContent {
|
|
fn from(output: SubagentToolOutput) -> Self {
|
|
serde_json::to_string(&output)
|
|
.expect("Failed to serialize SubagentToolOutput")
|
|
.into()
|
|
}
|
|
}
|
|
|
|
/// Tool that spawns a subagent thread to work on a task.
|
|
pub struct SubagentTool {
|
|
parent_thread: WeakEntity<Thread>,
|
|
environment: Rc<dyn ThreadEnvironment>,
|
|
}
|
|
|
|
impl SubagentTool {
|
|
pub fn new(parent_thread: WeakEntity<Thread>, environment: Rc<dyn ThreadEnvironment>) -> Self {
|
|
Self {
|
|
parent_thread,
|
|
environment,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AgentTool for SubagentTool {
|
|
type Input = SubagentToolInput;
|
|
type Output = SubagentToolOutput;
|
|
|
|
const NAME: &'static str = "subagent";
|
|
|
|
fn kind() -> acp::ToolKind {
|
|
acp::ToolKind::Other
|
|
}
|
|
|
|
fn initial_title(
|
|
&self,
|
|
input: Result<Self::Input, serde_json::Value>,
|
|
_cx: &mut App,
|
|
) -> SharedString {
|
|
input
|
|
.map(|i| i.label.into())
|
|
.unwrap_or_else(|_| "Subagent".into())
|
|
}
|
|
|
|
fn run(
|
|
self: Arc<Self>,
|
|
input: Self::Input,
|
|
event_stream: ToolCallEventStream,
|
|
cx: &mut App,
|
|
) -> Task<Result<SubagentToolOutput>> {
|
|
let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
|
|
return Task::ready(Err(anyhow!("Parent thread no longer exists")));
|
|
};
|
|
|
|
let subagent = match self.environment.create_subagent(
|
|
parent_thread_entity,
|
|
input.label,
|
|
input.prompt,
|
|
input.timeout.map(|secs| Duration::from_secs(secs)),
|
|
cx,
|
|
) {
|
|
Ok(subagent) => subagent,
|
|
Err(err) => return Task::ready(Err(err)),
|
|
};
|
|
|
|
let subagent_session_id = subagent.id();
|
|
|
|
event_stream.subagent_spawned(subagent_session_id.clone());
|
|
let meta = acp::Meta::from_iter([(
|
|
SUBAGENT_SESSION_ID_META_KEY.into(),
|
|
subagent_session_id.to_string().into(),
|
|
)]);
|
|
event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
|
|
|
|
cx.spawn(async move |cx| {
|
|
let output = subagent.wait_for_output(cx).await?;
|
|
Ok(SubagentToolOutput {
|
|
session_id: subagent_session_id,
|
|
output,
|
|
})
|
|
})
|
|
}
|
|
|
|
fn replay(
|
|
&self,
|
|
_input: Self::Input,
|
|
output: Self::Output,
|
|
event_stream: ToolCallEventStream,
|
|
_cx: &mut App,
|
|
) -> Result<()> {
|
|
event_stream.subagent_spawned(output.session_id.clone());
|
|
let meta = acp::Meta::from_iter([(
|
|
SUBAGENT_SESSION_ID_META_KEY.into(),
|
|
output.session_id.to_string().into(),
|
|
)]);
|
|
event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
|
|
Ok(())
|
|
}
|
|
}
|