Sketch out new Agent traits

Ben Brandt and Antonio Scandurra created

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

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(+)

Detailed changes

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"

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"

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"] }

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<Output = Result<Vec<AgentThreadSummary>>>;
+    fn create_thread(&self) -> impl Future<Output = Result<Self::Thread>>;
+    fn open_thread(&self, id: ThreadId) -> impl Future<Output = Result<Self::Thread>>;
+}
+
+pub trait AgentThread: 'static {
+    fn entries(&self) -> impl Future<Output = Result<Vec<AgentThreadEntry>>>;
+    fn send(&self, message: Message) -> impl Future<Output = Result<()>>;
+    fn on_message(
+        &self,
+        handler: impl AsyncFn(Role, BoxStream<'static, Result<MessageChunk>>) -> Result<()>,
+    );
+}
+
+pub struct ThreadId(Uuid);
+
+pub struct FileVersion(u64);
+
+pub struct AgentThreadSummary {
+    pub id: ThreadId,
+    pub title: String,
+    pub created_at: DateTime<Utc>,
+}
+
+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<MessageChunk>,
+}
+
+pub enum MessageChunk {
+    Text {
+        chunk: String,
+    },
+    File {
+        content: FileContent,
+    },
+    Directory {
+        path: PathBuf,
+        contents: Vec<FileContent>,
+    },
+    Symbol {
+        path: PathBuf,
+        range: Range<u64>,
+        version: FileVersion,
+        name: String,
+        content: String,
+    },
+    Thread {
+        title: String,
+        content: Vec<AgentThreadEntry>,
+    },
+    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<T: Agent> {
+    agent: Arc<T>,
+    threads: Vec<AgentThreadSummary>,
+}
+
+impl<T: Agent> ThreadStore<T> {
+    pub async fn load(agent: Arc<T>, cx: &mut AsyncApp) -> Result<Entity<Self>> {
+        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<Self>,
+    ) -> Task<Result<Entity<Thread<T::Thread>>>> {
+        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<Self>) -> Task<Result<Entity<Thread<T::Thread>>>> {
+        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<T: AgentThread> {
+    agent_thread: Arc<T>,
+    entries: Vec<ThreadEntry>,
+    next_entry_id: ThreadEntryId,
+}
+
+impl<T: AgentThread> Thread<T> {
+    pub async fn load(agent_thread: Arc<T>, cx: &mut AsyncApp) -> Result<Entity<Self>> {
+        let entries = agent_thread.entries().await?;
+        cx.new(|cx| Self::new(agent_thread, entries, cx))
+    }
+
+    pub fn new(
+        agent_thread: Arc<T>,
+        entries: Vec<AgentThreadEntry>,
+        cx: &mut Context<Self>,
+    ) -> 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<Self>,
+        role: Role,
+        mut chunks: BoxStream<'static, Result<MessageChunk>>,
+        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<Self>) -> Task<Result<()>> {
+        let agent_thread = self.agent_thread.clone();
+        cx.spawn(async move |_, cx| agent_thread.send(message).await)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+}