From ec376e0b619f81942040c31b32688f3a54567b2d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 24 Jun 2025 12:26:40 +0200 Subject: [PATCH] Sketch out new Agent traits Co-authored-by: Antonio Scandurra --- Cargo.lock | 11 ++ Cargo.toml | 3 + crates/agent2/Cargo.toml | 28 +++++ crates/agent2/LICENSE-GPL | 1 + crates/agent2/src/agent2.rs | 231 ++++++++++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 crates/agent2/Cargo.toml create mode 120000 crates/agent2/LICENSE-GPL create mode 100644 crates/agent2/src/agent2.rs diff --git a/Cargo.lock b/Cargo.lock index 922fed0ae45dfac97b4c50dce82fc540fa96cc15..fbad211c169d7fe285bfa83aeddf952f7e0f5c53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,17 @@ dependencies = [ "zstd", ] +[[package]] +name = "agent2" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "futures 0.3.31", + "gpui", + "uuid", +] + [[package]] name = "agent_settings" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8de3ad9f74033d5e03825849579ef4a9801b30d3..9ef922b1d995a1541a49c273c5b9994677dba419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/activity_indicator", "crates/agent_ui", "crates/agent", + "crates/agent2", "crates/agent_settings", "crates/anthropic", "crates/askpass", @@ -215,6 +216,7 @@ edition = "2024" activity_indicator = { path = "crates/activity_indicator" } agent = { path = "crates/agent" } +agent2 = { path = "crates/agent2" } agent_ui = { path = "crates/agent_ui" } agent_settings = { path = "crates/agent_settings" } ai = { path = "crates/ai" } @@ -394,6 +396,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # +agentic-coding-protocol = { path = "../agentic-coding-protocol" } aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e5643cb9eef39d9feea4446f304ff76bbea7dd34 --- /dev/null +++ b/crates/agent2/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "agent2" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/agent2.rs" +doctest = false + +[features] +test-support = [ + "gpui/test-support", +] + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +futures.workspace = true +gpui.workspace = true +uuid.workspace = true + +[dev-dependencies] +gpui = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/LICENSE-GPL b/crates/agent2/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/agent2/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs new file mode 100644 index 0000000000000000000000000000000000000000..80c91afd5ed7ea40f8fe2f95344b00ee6aed52e4 --- /dev/null +++ b/crates/agent2/src/agent2.rs @@ -0,0 +1,231 @@ +use anyhow::{Result, anyhow}; +use chrono::{DateTime, Utc}; +use futures::{StreamExt, stream::BoxStream}; +use gpui::{AppContext, AsyncApp, Context, Entity, Task, WeakEntity}; +use std::{ops::Range, path::PathBuf, sync::Arc}; +use uuid::Uuid; + +pub trait Agent: 'static { + type Thread: AgentThread; + + fn threads(&self) -> impl Future>>; + fn create_thread(&self) -> impl Future>; + fn open_thread(&self, id: ThreadId) -> impl Future>; +} + +pub trait AgentThread: 'static { + fn entries(&self) -> impl Future>>; + fn send(&self, message: Message) -> impl Future>; + fn on_message( + &self, + handler: impl AsyncFn(Role, BoxStream<'static, Result>) -> Result<()>, + ); +} + +pub struct ThreadId(Uuid); + +pub struct FileVersion(u64); + +pub struct AgentThreadSummary { + pub id: ThreadId, + pub title: String, + pub created_at: DateTime, +} + +pub struct FileContent { + pub path: PathBuf, + pub version: FileVersion, + pub content: String, +} + +pub enum Role { + User, + Assistant, +} + +pub struct Message { + pub role: Role, + pub chunks: Vec, +} + +pub enum MessageChunk { + Text { + chunk: String, + }, + File { + content: FileContent, + }, + Directory { + path: PathBuf, + contents: Vec, + }, + Symbol { + path: PathBuf, + range: Range, + version: FileVersion, + name: String, + content: String, + }, + Thread { + title: String, + content: Vec, + }, + Fetch { + url: String, + content: String, + }, +} + +pub enum AgentThreadEntry { + Message(Message), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ThreadEntryId(usize); + +impl ThreadEntryId { + pub fn post_inc(&mut self) -> Self { + let id = *self; + self.0 += 1; + id + } +} + +pub struct ThreadEntry { + pub id: ThreadEntryId, + pub entry: AgentThreadEntry, +} + +pub struct ThreadStore { + agent: Arc, + threads: Vec, +} + +impl ThreadStore { + pub async fn load(agent: Arc, cx: &mut AsyncApp) -> Result> { + let threads = agent.threads().await?; + cx.new(|cx| Self { agent, threads }) + } + + /// Returns the threads in reverse chronological order. + pub fn threads(&self) -> &[AgentThreadSummary] { + &self.threads + } + + /// Opens a thread with the given ID. + pub fn open_thread( + &self, + id: ThreadId, + cx: &mut Context, + ) -> Task>>> { + let agent = self.agent.clone(); + cx.spawn(async move |_, cx| { + let agent_thread = agent.open_thread(id).await?; + Thread::load(Arc::new(agent_thread), cx).await + }) + } + + /// Creates a new thread. + pub fn create_thread(&self, cx: &mut Context) -> Task>>> { + let agent = self.agent.clone(); + cx.spawn(async move |_, cx| { + let agent_thread = agent.create_thread().await?; + Thread::load(Arc::new(agent_thread), cx).await + }) + } +} + +pub struct Thread { + agent_thread: Arc, + entries: Vec, + next_entry_id: ThreadEntryId, +} + +impl Thread { + pub async fn load(agent_thread: Arc, cx: &mut AsyncApp) -> Result> { + let entries = agent_thread.entries().await?; + cx.new(|cx| Self::new(agent_thread, entries, cx)) + } + + pub fn new( + agent_thread: Arc, + entries: Vec, + cx: &mut Context, + ) -> Self { + agent_thread.on_message({ + let this = cx.weak_entity(); + let cx = cx.to_async(); + async move |role, chunks| { + Self::handle_message(this.clone(), role, chunks, &mut cx.clone()).await + } + }); + let mut next_entry_id = ThreadEntryId(0); + Self { + agent_thread, + entries: entries + .into_iter() + .map(|entry| ThreadEntry { + id: next_entry_id.post_inc(), + entry, + }) + .collect(), + next_entry_id, + } + } + + async fn handle_message( + this: WeakEntity, + role: Role, + mut chunks: BoxStream<'static, Result>, + cx: &mut AsyncApp, + ) -> Result<()> { + let entry_id = this.update(cx, |this, cx| { + let entry_id = this.next_entry_id.post_inc(); + this.entries.push(ThreadEntry { + id: entry_id, + entry: AgentThreadEntry::Message(Message { + role, + chunks: Vec::new(), + }), + }); + cx.notify(); + entry_id + })?; + + while let Some(chunk) = chunks.next().await { + match chunk { + Ok(chunk) => { + this.update(cx, |this, cx| { + let ix = this + .entries + .binary_search_by_key(&entry_id, |entry| entry.id) + .map_err(|_| anyhow!("message not found"))?; + let AgentThreadEntry::Message(message) = &mut this.entries[ix].entry else { + unreachable!() + }; + message.chunks.push(chunk); + cx.notify(); + anyhow::Ok(()) + })??; + } + Err(err) => todo!("show error"), + } + } + + Ok(()) + } + + pub fn entries(&self) -> &[ThreadEntry] { + &self.entries + } + + pub fn send(&mut self, message: Message, cx: &mut Context) -> Task> { + let agent_thread = self.agent_thread.clone(); + cx.spawn(async move |_, cx| agent_thread.send(message).await) + } +} + +#[cfg(test)] +mod tests { + use super::*; +}