@@ -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"
@@ -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::*;
+}